Skip to main content

零成本类型安全

提供更多便利性和正确性,减少样板代码

通过在 SvelteKit 应用中添加类型注解,您可以在整个网络中实现完全的类型安全——页面中的 data 拥有从生成该数据的 load 函数返回值推断出的类型,而无需您显式地声明任何内容。这是那种让您会怀疑以往如何能没有它而生活的功能。

但如果我们甚至不需要注解呢?既然 loaddata 是框架的一部分,那么框架能不能为我们生成它们的类型呢?毕竟,这正是计算机的用武之地——做枯燥的部分,让我们专注于创造性任务。

从今天起,答案是肯定的:可以。

如果您使用的是 VSCode,只需将 Svelte 插件升级到最新版本,就再也不需要为 load 函数或 data 属性编写注解了。其他编辑器的扩展也可以使用此功能,只要它们支持语言服务器协议 (LSP) 和 TypeScript 插件。甚至我们最新版本的命令行诊断工具 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';

// 从路由树推断的类型
type 
type RouteParams = {
    slug: string;
}
RouteParams
= { slug: stringslug: string };
type type RouteId = "/blog/[slug]"RouteId = '/blog/[slug]'; type type PageParentData = {}PageParentData = {}; // PageServerLoad 类型扩展了通用 Load 类型,并使用我们拥有的信息填充了泛型 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>;
// load 函数的输入参数类型 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];
// load 函数的返回值类型 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 目录——那样会让代码变得杂乱,而没有人喜欢乱糟糟的代码。相反,我们使用 TypeScript 的一个特性叫 rootDirs,它允许我们将「虚拟」目录映射到实际目录上。通过将 rootDirs 设置为项目根目录(默认)以及 .svelte-kit/types(所有生成类型的输出文件夹),然后在其中镜像路由结构,就能实现所需的行为:

// 磁盘上的文件结构:
.svelte-kit/
├ types/
│ ├ src/
│ │ ├ routes/
│ │ │ ├ blog/
│ │ │ │ ├ [slug]/
│ │ │ │ │ └ $types.d.ts
src/
├ routes/
│ ├ blog/
│ │ ├ [slug]/
│ │ │ ├ +page.server.ts
│ │ │ └ +page.svelte
// TypeScript 看到的文件结构:
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 中提供一流的类型安全,并为您提供工具,让体验尽可能流畅——无论您使用的是 TypeScript,还是通过 JSDoc 提供类型的 JavaScript,这种工具都能完美扩展到较大的 Svelte 代码库中。