ゼロ努力で実現する型安全
より便利で正確に、ボイラープレートを削減
SvelteKit アプリに型注釈を散りばめることで、ネットワーク全体で完全な型安全性を実現できます。ページの data
は、明示的に宣言することなく、そのデータを生成した load
関数の戻り値から推論される型を持っています。これは一度体験すると、どうやってこれまでそれなしでやってきたのか不思議に思うようなものです。
しかし、注釈さえ必要ないとしたらどうでしょう? load
と data
はフレームワークの一部であるため、フレームワークがそれらの型を定義できないでしょうか?結局のところ、コンピューターはつまらない作業をして、私たちが創造的なことに集中できるようにするためのものです。
今日から、それが可能になります。
VSCode を使用している場合は、Svelte 拡張機能を最新バージョンにアップグレードするだけで、load
関数や data
プロパティに二度と注釈を付ける必要はありません。他のエディターの拡張機能も、Language Server Protocol と 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)
};
}
これでうまく動作しますが、もっと改善できます。パラメーターの名前が post
ではなく slug
(ファイル名の [slug]
に由来)であるにもかかわらず、誤って event.params.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
の型も絞り込まれます。ここでは LayoutServerLoadEvent
と区別するために、PageServerLoadEvent
を使用していることに注意してください。
データの読み込みが完了したら、+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 はルートツリーをたどることで、正しいパラメーターや親データなどを推論できます。結果は各ルートごとに 1 つの $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
ディレクトリに書き込みません。それは乱雑になり、誰も乱雑なコードを好みません。代わりに、TypeScript の機能である rootDirs
を使用して、「仮想」ディレクトリを実際のディレクトリにマップします。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 プラグイン(.ts/js
ファイル内から Svelte ファイルを TypeScript が理解できるようにする)の両方を変更する必要がありました。両方で、正しい位置に正しい型を自動的に挿入し、元の型のないファイルではなく、仮想の拡張ファイルを使用するように TypeScript に指示しています。これにより、生成された位置と元の位置を相互にマッピングすることで、目的の結果が得られます。svelte-check
は内部で言語サーバーの一部を再利用するため、追加の調整なしでこの機能を利用できます。
この機能のインスピレーションを与えてくれた Next.js チームに感謝します。inspiring
今後の展望
今後は、HTML 内やプログラムで goto
を呼び出す場合など、SvelteKit のより多くの領域を型安全にすることを目指します。
TypeScript は JavaScript の世界を席巻しており、私たちはそれを支持しています!私たちは SvelteKit での最優先の型安全性を深く重視しており、TypeScript を使用しているか、JSDoc を介して型付けされた JavaScript を使用しているかに関わらず、Svelte コードベースを大規模に拡張できるような、可能な限りスムーズな体験を実現するためのツールを提供しています。