零成本类型安全
更多便利性和正确性,更少的样板代码
通过在您的 SvelteKit 应用中添加类型注解,您可以在整个网络中获得完整的类型安全——页面中的data
的类型是从生成该数据的load
函数的返回值推断出来的,而无需您显式声明任何内容。这是您开始想知道没有它您是如何生存下来的那些事情之一。
但是,如果我们甚至不需要注解呢?由于load
和data
是框架的一部分,框架能否为我们键入它们?毕竟,这就是计算机的用途——完成枯燥的部分,以便我们专注于创意工作。
截至今天,可以。
如果您使用的是 VSCode,只需将 Svelte 扩展更新到最新版本,您就永远不必再为您的load
函数或data
属性添加注解了。其他编辑器的扩展也可以使用此功能,只要它们支持语言服务器协议和 TypeScript 插件即可。它甚至适用于我们最新版本的 CLI 诊断工具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';
// types inferred from the routing tree
type type RouteParams = {
slug: string;
}
RouteParams = { slug: string
slug: 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*/ any
ReturnType<
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
无需类型即可实现类型安全
由于自动类型生成,我们获得了高级类型安全。但是,如果我们可以完全省略编写类型,那不是很好吗?截至今天,您可以做到这一点。
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 中的一流类型安全,并且为您提供工具,使体验尽可能流畅——一种也可以很好地扩展到更大的 Svelte 代码库的体验——无论您是使用 TypeScript 还是通过 JSDoc 使用类型化 JavaScript。