跳至主要内容

零成本类型安全

更多便利性和正确性,更少的样板代码

通过在您的 SvelteKit 应用中添加类型注解,您可以在整个网络中获得完整的类型安全——页面中的data的类型是从生成该数据的load函数的返回值推断出来的,而无需您显式声明任何内容。这是您开始想知道没有它您是如何生存下来的那些事情之一。

但是,如果我们甚至不需要注解呢?由于loaddata是框架的一部分,框架能否为我们键入它们?毕竟,这就是计算机的用途——完成枯燥的部分,以便我们专注于创意工作。

截至今天,可以。

如果您使用的是 VSCode,只需将 Svelte 扩展更新到最新版本,您就永远不必再为您的load函数或data属性添加注解了。其他编辑器的扩展也可以使用此功能,只要它们支持语言服务器协议和 TypeScript 插件即可。它甚至适用于我们最新版本的 CLI 诊断工具svelte-check

在深入探讨之前,让我们回顾一下类型安全如何在 SvelteKit 中工作。

生成的类型

在 SvelteKit 中,您可以在load函数中获取页面的数据。您可以使用@sveltejs/kit中的ServerLoadEvent来键入事件。

src/routes/blog/[slug]/+page.server
import type { interface ServerLoadEvent<Params extends Partial<Record<string, string>> = Partial<Record<string, string>>, ParentData extends Record<string, any> = Record<string, any>, RouteId extends string | null = string | null>ServerLoadEvent } from '@sveltejs/kit';

export async function 
function load(event: ServerLoadEvent): Promise<{
    post: string;
}>
load
(event: ServerLoadEvent<Partial<Record<string, string>>, Record<string, any>, string | null>event: interface ServerLoadEvent<Params extends Partial<Record<string, string>> = Partial<Record<string, string>>, ParentData extends Record<string, any> = Record<string, any>, RouteId extends string | null = string | null>ServerLoadEvent) {
return { post: stringpost: await
const database: {
    getPost(slug: string | undefined): Promise<string>;
}
database
.function getPost(slug: string | undefined): Promise<string>getPost(event: ServerLoadEvent<Partial<Record<string, string>>, Record<string, any>, string | null>event.RequestEvent<Partial<Record<string, string>>, string | null>.params: Partial<Record<string, string>>

The parameters of the current route - e.g. for a route like /blog/[slug], a { slug: string } object

params
.string | undefinedpost)
}; }

这可以工作,但我们可以做得更好。请注意,我们意外地写了event.params.post,即使参数名为slug(因为文件名中的[slug]),而不是post。您可以通过向ServerLoadEvent添加泛型参数来自行键入params,但这很脆弱。

这就是我们的自动类型生成发挥作用的地方。每个路由目录都有一个隐藏的$types.d.ts文件,其中包含特定于路由的类型。

src/routes/blog/[slug]/+page.server
import type { ServerLoadEvent } from '@sveltejs/kit';
import type { import PageServerLoadEventPageServerLoadEvent } from './$types';

export async function 
function load(event: PageServerLoadEvent): Promise<{
    post: any;
}>
load
(event: PageServerLoadEventevent: import PageServerLoadEventPageServerLoadEvent) {
return { post: await database.getPost(event.params.post) post: anypost: await database.getPost(event: PageServerLoadEventevent.params.slug) }; }

这揭示了我们的类型错误,因为它现在在params.post属性访问上出错。除了缩小参数类型外,它还缩小了await event.parent()和从服务器load函数传递到通用load函数的data的类型。请注意,我们现在使用的是PageServerLoadEvent,以将其与LayoutServerLoadEvent区分开来。

加载完数据后,我们希望在我们的+page.svelte中显示它。相同的类型生成机制确保data的类型是正确的。

src/routes/blog/[slug]/+page
<script lang="ts">
	import type { PageData } from './$types';

	export let data: PageData;
</script>

<h1>{data.post.title}</h1>

<div>{@html data.post.content}</div>

虚拟文件

在运行开发服务器或构建时,会自动生成类型。由于基于文件系统的路由,SvelteKit 能够通过遍历路由树来推断正确参数或父级数据等内容。结果输出到每个路由的一个$types.d.ts文件中,其大致如下所示。

$types.d
import type * as module "@sveltejs/kit"Kit from '@sveltejs/kit';

// types inferred from the routing tree
type 
type RouteParams = {
    slug: string;
}
RouteParams
= { slug: stringslug: string };
type type RouteId = "/blog/[slug]"RouteId = '/blog/[slug]'; type type PageParentData = {}PageParentData = {}; // PageServerLoad type extends the generic Load type and fills its generics with the info we have export type type PageServerLoad = (event: Kit.ServerLoadEvent<RouteParams, PageParentData, string | null>) => MaybePromise<"/blog/[slug]">PageServerLoad = module "@sveltejs/kit"Kit.type ServerLoad<Params extends Partial<Record<string, string>> = Partial<Record<string, string>>, ParentData extends Record<string, any> = Record<string, any>, OutputData extends Record<string, any> | void = void | Record<...>, RouteId extends string | null = string | null> = (event: Kit.ServerLoadEvent<Params, ParentData, RouteId>) => MaybePromise<OutputData>

The generic form of PageServerLoad and LayoutServerLoad. You should import those from ./$types (see generated types) rather than using ServerLoad directly.

ServerLoad
<
type RouteParams = {
    slug: string;
}
RouteParams
, type PageParentData = {}PageParentData, type RouteId = "/blog/[slug]"RouteId>;
// The input parameter type of the load function export type type PageServerLoadEvent = Kit.ServerLoadEvent<RouteParams, PageParentData, string | null>PageServerLoadEvent = type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never

Obtain the parameters of a function type in a tuple

Parameters
<type PageServerLoad = (event: Kit.ServerLoadEvent<RouteParams, PageParentData, string | null>) => MaybePromise<"/blog/[slug]">PageServerLoad>[0];
// The return type of the load function export type type PageData = Kit.ReturnType<any>PageData = module "@sveltejs/kit"Kit.type Kit.ReturnType = /*unresolved*/ anyReturnType< typeof import('../src/routes/blog/[slug]/+page.server.js').load >;

我们实际上并没有将$types.d.ts写入您的src目录——那会很乱,而且没有人喜欢乱七八糟的代码。相反,我们使用了一个名为rootDirs的 TypeScript 功能,它允许我们将“虚拟”目录映射到真实目录。通过将rootDirs设置为项目根目录(默认值)以及.svelte-kit/types(所有生成类型的输出文件夹),然后在其中镜像路由结构,我们可以获得所需的特性。

// on disk:
.svelte-kit/
├ types/
│ ├ src/
│ │ ├ routes/
│ │ │ ├ blog/
│ │ │ │ ├ [slug]/
│ │ │ │ │ └ $types.d.ts
src/
├ routes/
│ ├ blog/
│ │ ├ [slug]/
│ │ │ ├ +page.server.ts
│ │ │ └ +page.svelte
// what TypeScript sees:
src/
├ routes/
│ ├ blog/
│ │ ├ [slug]/
│ │ │ ├ $types.d.ts
│ │ │ ├ +page.server.ts
│ │ │ └ +page.svelte

无需类型即可实现类型安全

由于自动类型生成,我们获得了高级类型安全。但是,如果我们可以完全省略编写类型,那不是很好吗?截至今天,您可以做到这一点。

src/routes/blog/[slug]/+page.server
import type { PageServerLoadEvent } from './$types';

export async function 
function load(event: any): Promise<{
    post: any;
}>
load
(event: anyevent: PageServerLoadEvent) {
return { post: anypost: await database.getPost(event: anyevent.params.post) }; }
src/routes/blog/[slug]/+page
<script lang="ts">
	import type { PageData } from './$types';
	export let data: PageData;
	export let data;
</script>

虽然这非常方便,但这不仅仅是关于这一点。它也与正确性有关:在复制粘贴代码时,很容易意外地将PageServerLoadEventLayoutServerLoadEventPageLoadEvent混淆,例如——具有细微差别的类似类型。Svelte 的主要见解是,通过以声明的方式编写代码,我们可以让机器为我们完成大部分工作,正确且高效。这没有什么不同——通过利用强大的框架约定(如+page文件),我们可以使做正确的事情比做错误的事情更容易。

这适用于 SvelteKit 文件的所有导出(+page+layout+serverhooksparams等)以及+page/layout.svelte文件中的dataformsnapshot属性。

要在 VS Code 中使用此功能,请安装最新版本的 Svelte for VS Code 扩展。对于其他 IDE,请使用最新版本的 Svelte 语言服务器和 Svelte TypeScript 插件。除了编辑器之外,我们的命令行工具svelte-check自 3.1.1 版本起也知道如何添加这些注解。

它是如何工作的?

要使它工作,需要对语言服务器(为 Svelte 文件中的 IntelliSense 提供支持)和 TypeScript 插件(使 TypeScript 能够从.ts/js文件内部理解 Svelte 文件)进行更改。在两者中,我们都在正确的位置自动插入正确的类型,并告诉 TypeScript 使用我们增强的虚拟文件而不是原始的未键入文件。这与将生成的和原始位置来回映射相结合,可以得到预期的结果。由于svelte-check在幕后重用了语言服务器的部分功能,因此它无需进一步调整即可免费获得此功能。

我们要感谢 Next.js 团队启发了此功能。

接下来是什么

未来,我们希望研究如何使 SvelteKit 的更多领域类型安全——例如链接,无论是在您的 HTML 中还是通过以编程方式调用goto

TypeScript 正在吞噬 JavaScript 世界——我们对此表示支持!我们非常重视 SvelteKit 中的一流类型安全,并且为您提供工具,使体验尽可能流畅——一种也可以很好地扩展到更大的 Svelte 代码库的体验——无论您是使用 TypeScript 还是通过 JSDoc 使用类型化 JavaScript。