メインコンテンツへスキップ

ゼロ努力で実現する型安全

より便利で正確に、ボイラープレートを削減

SvelteKit アプリに型注釈を散りばめることで、ネットワーク全体で完全な型安全性を実現できます。ページの data は、明示的に宣言することなく、そのデータを生成した load 関数の戻り値から推論される型を持っています。これは一度体験すると、どうやってこれまでそれなしでやってきたのか不思議に思うようなものです。

しかし、注釈さえ必要ないとしたらどうでしょう? loaddata はフレームワークの一部であるため、フレームワークがそれらの型を定義できないでしょうか?結局のところ、コンピューターはつまらない作業をして、私たちが創造的なことに集中できるようにするためのものです。

今日から、それが可能になります。

VSCode を使用している場合は、Svelte 拡張機能を最新バージョンにアップグレードするだけで、load 関数や data プロパティに二度と注釈を付ける必要はありません。他のエディターの拡張機能も、Language Server Protocol と 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)
}; }

これでうまく動作しますが、もっと改善できます。パラメーターの名前が post ではなく slug(ファイル名の [slug] に由来)であるにもかかわらず、誤って event.params.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 の型も絞り込まれます。ここでは LayoutServerLoadEvent と区別するために、PageServerLoadEvent を使用していることに注意してください。

データの読み込みが完了したら、+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 はルートツリーをたどることで、正しいパラメーターや親データなどを推論できます。結果は各ルートごとに 1 つの $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.tssrc ディレクトリに書き込みません。それは乱雑になり、誰も乱雑なコードを好みません。代わりに、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

型なしでの型安全

自動型生成のおかげで、高度な型安全性を実現できます。型をまったく記述しなくても済むとしたら素晴らしいでしょう。今日から、それが可能になりました。

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 プラグイン(.ts/js ファイル内から Svelte ファイルを TypeScript が理解できるようにする)の両方を変更する必要がありました。両方で、正しい位置に正しい型を自動的に挿入し、元の型のないファイルではなく、仮想の拡張ファイルを使用するように TypeScript に指示しています。これにより、生成された位置と元の位置を相互にマッピングすることで、目的の結果が得られます。svelte-check は内部で言語サーバーの一部を再利用するため、追加の調整なしでこの機能を利用できます。

この機能のインスピレーションを与えてくれた Next.js チームに感謝します。inspiring

今後の展望

今後は、HTML 内やプログラムで goto を呼び出す場合など、SvelteKit のより多くの領域を型安全にすることを目指します。

TypeScript は JavaScript の世界を席巻しており、私たちはそれを支持しています!私たちは SvelteKit での最優先の型安全性を深く重視しており、TypeScript を使用しているか、JSDoc を介して型付けされた JavaScript を使用しているかに関わらず、Svelte コードベースを大規模に拡張できるような、可能な限りスムーズな体験を実現するためのツールを提供しています。