跳至主要內容

零成本的型別安全

更方便、更準確,更少樣板程式碼

透過在您的 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 中的一流型別安全,並且我們為您提供工具,使體驗盡可能順暢 — 無論您是透過 JSDoc 使用 TypeScript 還是類型化的 JavaScript,都能完美地擴展到更大的 Svelte 程式碼庫。