零成本类型安全
提供更多便利性和正确性,减少样板代码
通过在 SvelteKit 应用中添加类型注解,您可以在整个网络中实现完全的类型安全——页面中的 data
拥有从生成该数据的 load
函数返回值推断出的类型,而无需您显式地声明任何内容。这是那种让您会怀疑以往如何能没有它而生活的功能。
但如果我们甚至不需要注解呢?既然 load
和 data
是框架的一部分,那么框架能不能为我们生成它们的类型呢?毕竟,这正是计算机的用武之地——做枯燥的部分,让我们专注于创造性任务。
从今天起,答案是肯定的:可以。
如果您使用的是 VSCode,只需将 Svelte 插件升级到最新版本,就再也不需要为 load
函数或 data
属性编写注解了。其他编辑器的扩展也可以使用此功能,只要它们支持语言服务器协议 (LSP) 和 TypeScript 插件。甚至我们最新版本的命令行诊断工具 svelte-check
也支持这个功能!
在深入了解之前,让我们回顾一下 SvelteKit 中类型安全的工作原理。
自动生成的类型
在 SvelteKit 中,页面的数据通过 load
函数获取。您 可以 使用 @sveltejs/kit
中的 ServerLoadEvent
为事件添加类型:
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: string
post: 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 | undefined
post)
};
}
这可以工作,但我们可以做得更好。注意,我们不小心写成了 event.params.post
,尽管参数应该叫做 slug
(因为文件名中的 [slug]
),而不是 post
。您可以通过给 ServerLoadEvent
添加一个泛型参数来手动为 params
添加类型,但这很脆弱。
这时,我们的自动类型生成功能派上用场了。每个路由目录都有一个隐藏的 $types.d.ts
文件,里面包含特定路由的类型:
import type { ServerLoadEvent } from '@sveltejs/kit';
import type { import PageServerLoadEvent
PageServerLoadEvent } from './$types';
export async function function load(event: PageServerLoadEvent): Promise<{
post: any;
}>
load(event: PageServerLoadEvent
event: import PageServerLoadEvent
PageServerLoadEvent) {
return {
post: await database.getPost(event.params.post)
post: any
post: await database.getPost(event: PageServerLoadEvent
event.params.slug)
};
}
这暴露了我们的拼写错误,因为现在在访问 params.post
属性时会导致错误。除了收窄参数的类型,本功能还会收窄 await event.parent()
和从服务端 load
函数传递到通用 load
函数的 data
的类型。请注意,我们现在使用的是 PageServerLoadEvent
,以将其与 LayoutServerLoadEvent
区分开来。
在加载完数据后,我们需要在 +page.svelte
中显示它。相同的类型生成机制确保了 data
的类型是正确的:
<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
文件中,其内容大致如下:
import type * as module "@sveltejs/kit"
Kit from '@sveltejs/kit';
// 从路由树推断的类型
type type RouteParams = {
slug: string;
}
RouteParams = { slug: string
slug: 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*/ any
ReturnType<
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
无类型的类型安全
感谢自动类型生成技术,我们得到了高级的类型安全。但是,如果我们可以完全省略编写类型会怎么样?从今天起,您完全可以做到:
import type { PageServerLoadEvent } from './$types';
export async function function load(event: any): Promise<{
post: any;
}>
load(event: any
event: PageServerLoadEvent) {
return {
post: any
post: await database.getPost(event: any
event.params.post)
};
}
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
export let data;
</script>
虽然这超级方便,但这不仅仅是为了方便。它还关乎 正确性:复制和粘贴代码时,可能很容易不小心将 PageServerLoadEvent
与 LayoutServerLoadEvent
或 PageLoadEvent
混淆,这些类型类似却有微妙的差异。Svelte 的主要见解是,通过以声明性的方式编写代码,我们可以让计算机为我们完成大部分工作,并且做到正确高效。这并没有什么不同——通过利用强大的框架约定(如 +page
文件),我们可以让做正确的事情比做错误的事情更容易。
此功能适用于所有 SvelteKit 文件(+page
、+layout
、+server
、hooks
、params
等)中的导出,以及 +page/layout.svelte
文件中的 data
、form
和 snapshot
属性。
要在 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 代码库中。