Skip to main content

Zero-effort type safety

ボイラープレートなしで、より便利に、より正しく

SvelteKit アプリに型アノテーションをたくさん書くと、ネットワークをまたいでも完全な型安全性が手に入ります — あなたのページの data には、その data を生成する load 関数の戻り値から推論された型があり、明示的に何かを宣言する必要はありません。これなしで今までどうやって生活してきたのだろう、と考えさせられるようなことの1つです。

でも、型アノテーションが不要になったとしたら? loaddata はフレームワークの一部ですし、フレームワークが私たちのために型付けできないものでしょうか? これは結局、コンピューターが何のためにあるのか、ということです — 退屈なことをやってくれるから、私たちはクリエイティブなことに集中することができるのです。

そして今日現在、それができるようになりました。

VSCode をお使いでしたら、Svelte extension を最新バージョンにアップグレードするだけです。これでもう今後、load 関数や data プロパティにアノテーションを付ける必要はありません。他のエディタ向けの Extension でも、それが Language Server Protocol と TypeScript plugin をサポートしていればこの機能を使うことができます。CLI 診断ツール svelte-check の最新バージョンでも動作します!

詳細に入る前に、SvelteKit の型安全性の仕組みについておさらいしましょう。

Generated types

SvelteKit では、load 関数でページの data を取得します。@sveltejs/kit から ServerLoadEvent をインポートして、この event に型を付けることができます:

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 に型付けすることもできますが、柔軟性がなく壊れやすいです。

そこで、自動型生成の出番です。全てのルート(route)ディレクトリには、それぞれのルート固有(route-specific)の型を持つ $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 プロパティにアクセスしようとするとエラーとなり、打ち間違い(typo)がわかるようになります。パラメーターの型を絞り込むだけでなく、await event.parent() の型や、server load 関数や universal load 関数から渡される data の型も絞り込むことができます。LayoutServerLoadEvent と区別するため、PageServerLoadEvent を使用していることにご注意ください。

data をロードしたあと、それを +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>

Virtual files

開発サーバー(dev server)、またはビルド(build)を実行しているときに、型が自動で生成されます。ファイルシステムベースルーティングのおかげで、SvelteKit はルートツリー(route tree)をトラバースし、正しいパラメーターや親の data を推論することができます。各ルート(route)ごとに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.ts を実際に src ディレクトリに書き込んでいるわけではありません — ちょっとごちゃごちゃしますし、ごちゃごちゃしたコードが好きな人はいません。代わりに、TypeScript の rootDirs という機能を使用し、‘virtual’ ディレクトリを実際のディレクトリにマップします。rootDirs に、プロジェクトの root (デフォルト) と、さらに .svelte-kit/types (全ての generated types の出力フォルダ) を設定し、その中でルート構造(route structure)をミラーリングすることで、この挙動を実現しています:

// 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

Type safety without types

自動型生成のおかげで、高度な型安全性を実現しています。ただ、もし型を書くのをすべて省略できるようになったとしたら素晴らしいと思いませんか?今日現在、まさにそれができるようになりました:

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 など) からのすべての export と、+page/layout.svelte ファイルの dataformsnapshot プロパティで動作します。

VS Code でこの機能を使用するには、Svelte for VS Code extension の最新バージョンをインストールしてください。他の IDE では、Svelte language server と Svelte TypeScript plugin の最新バージョンを使用してください。エディタ以外では、コマンドラインツール svelte-check でも、バージョン 3.1.1 以降であればこれらのアノテーションを追加する方法が組み込まれています。

How does it work?

この機能を実現するには、(Svelte ファイルのインテリセンスを行ってくれる) language server と、(TypeScript に .ts/js ファイルの内部から Svelte ファイルを理解させる) TypeScript plugin の両方を変更する必要がありました。両方とも、正しい型を正しいポジションに自動挿入し、オリジナルの型付けされていないファイルではなく拡張された仮想ファイルを使用するよう TypeScript に指示します。生成されたファイルのポジションとオリジナルファイルのポジションを前後にマッピングして組み合わせることで、これを実現しています。svelte-check は language server の一部を再利用しているため、調整することなくこの機能が使えます。

この機能は Next.js チームからインスパイアされました。Next.js チームに感謝します。

What’s next

将来的には、SvelteKit のさらに多くの領域を型安全にすることを検討したいと思っています — 例えばリンクは、HTML の中や、プログラム的に goto を呼び出していますよね。

TypeScript は JavaScript の世界を席巻しています — 私たちはそれに夢中です! 私たちは SvelteKit のファーストクラスの型安全性に深く取り組んでおり、TypeScript を使用するか JSDoc で型付けされた JavaScript を使用するかに関わらず、より大規模な Svelte コードベースにも美しくスケールすることができる、できる限り開発体験をスムーズにするツールを提供します。