SvelteとTypeScriptのユースケース 型安全な開発スタイルを整理する
Svelte で TypeScript を使った開発スタイルを実務で採用する際、「どこに型を付ければ安全になるのか」「どのパターンが保守しやすいのか」を判断する必要があります。本記事では、Props や状態管理、イベント、API 連携といった具体的なユースケースごとに、型安全な開発パターンを整理します。
実際の業務では、「TypeScript を導入したものの、どこまで型を付けるべきかわからない」「any で逃げてしまい、結局型安全にならない」といった課題に直面することがあります。検証と実装を重ねた結果、ユースケースごとに適切な型付けパターンが存在することがわかりました。
本記事が、Svelte プロジェクトで TypeScript を活用し、型安全と開発効率を両立させたいエンジニアの判断材料になれば幸いです。
ユースケース別の型安全パターン早見表
静的型付けを活用する場面を、開発の実務フローに沿って整理しました。
| # | ユースケース | 型なし開発の問題 | TypeScript による解決 | 実務での重要度 |
|---|---|---|---|---|
| 1 | Props の受け渡し | コンポーネント間で予期しない値が渡される | インターフェースで Props を明示 | 高 |
| 2 | 状態管理 (Store) | Store の値の構造が不明確 | Writable, Readable の型パラメータで保証 | 高 |
| 3 | イベント送信 | イベントペイロードの型が不明 | createEventDispatcher のジェネリクスで型安全 | 中 |
| 4 | API レスポンス | レスポンス構造の変更に気づけない | API 型定義でコンパイル時チェック | 高 |
| 5 | リアクティブ宣言 | 計算結果の型推論が曖昧 | 明示的な型注釈で予測可能に | 中 |
この表は即答用の概要です。詳細な判断基準と実装パターンは後段で解説します。
検証環境
- OS: macOS 15.2 (Sequoia)
- Node.js: 22.12.0 (LTS)
- TypeScript: 5.7.2
- 主要パッケージ:
- svelte: 5.2.3
- @sveltejs/kit: 2.10.0
- vite: 6.0.3
- 検証日: 2025 年 12 月 29 日
Svelte + TypeScript で型安全が求められる背景
フロントエンド開発における静的型付けの普及
2025 年現在、フロントエンド開発において TypeScript の採用率は 80% を超えています。単なるトレンドではなく、チーム開発やプロダクトの長期運用において、静的型付けが実質的な標準となっています。
Svelte はコンパイラベースのフレームワークとして、ビルド時に最適化された JavaScript を生成する特性を持ちます。この特性と TypeScript を組み合わせることで、以下の二重の安全網が構築できます。
mermaidflowchart LR
code["TypeScript<br/>コード"] --> tsc["TypeScript<br/>コンパイラ"]
tsc --> check1["型チェック"]
check1 --> svelte["Svelte<br/>コンパイラ"]
svelte --> check2["構文最適化"]
check2 --> output["最適化された<br/>JavaScript"]
TypeScript による型チェックで論理エラーを防ぎ、Svelte のコンパイラでランタイムの無駄を削減する、という二段階の品質担保が可能になります。
Svelte 特有のリアクティビティと型の関係
Svelte の大きな特徴は、$: 構文によるリアクティブ宣言です。変数の変更を自動検知して再計算・再レンダリングが走る仕組みですが、この便利さの裏には「どの変数がどの型を持つか」が不明確になるリスクがあります。
実際に試したところ、以下のような問題が発生しました。
typescript// リアクティブ宣言での型推論の曖昧さ
let count = 0; // number と推論される
$: doubled = count * 2; // number と推論
$: message = `Count is ${count}`; // string と推論
// しかし、以下のような変更があると...
count = "5"; // ❌ コンパイルエラーが出ないと事故の元
TypeScript を導入することで、変数の型が変わるタイミングでエディタが警告を出し、バグを未然に防げます。
UI/UX に影響を与える型安全性
型安全性は、単なる開発者体験の向上だけでなく、エンドユーザーの UI/UX にも直結します。
業務で問題になったケースとして、フォーム送信時に API のレスポンス構造が変更されていたことに気づかず、エラーメッセージが表示されない状態でリリースしてしまった経験があります。TypeScript で API レスポンスの型を定義していれば、ビルド時にエラーが検出され、未然に防げた問題でした。
mermaidsequenceDiagram
participant User as ユーザー
participant UI as UI コンポーネント
participant API as API
participant Type as 型チェック
User->>UI: フォーム送信
UI->>API: リクエスト送信
API->>UI: レスポンス(構造変更)
UI->>Type: 型チェック
Type-->>UI: ❌ 型不一致検出
UI->>User: エラー表示
型チェックにより、ユーザーに不具合が届く前にエラーを検出できるため、UI/UX の品質が向上します。
この章でわかること
- TypeScript と Svelte の組み合わせで二重の品質担保が可能
- リアクティブ宣言における型の重要性
- 型安全性が UI/UX に与える影響
つまずきポイント
- Svelte のリアクティブ宣言
$:で型推論が曖昧になりやすい - Props や Store の型を後から追加するのは手間がかかる
型なし開発で実務的に発生する課題
Props の型不一致によるランタイムエラー
JavaScript のみで Svelte コンポーネントを開発すると、親コンポーネントから子コンポーネントへ渡す Props の型が保証されません。
検証の結果、以下のようなエラーが本番環境で発生するリスクがあることがわかりました。
javascript<!-- UserCard.svelte (型なし) -->
<script>
export let user; // 何が渡されるか不明
export let onDelete; // 関数のはずだが保証なし
</script>
<div>
<h3>{user.name}</h3>
<button on:click={() => onDelete(user.id)}>削除</button>
</div>
javascript<!-- App.svelte (親コンポーネント) -->
<script>
import UserCard from './UserCard.svelte';
let user = { id: 1, username: 'taro' }; // ❌ name ではなく username
function handleDelete(id) {
console.log('Delete user:', id);
}
</script>
<UserCard {user} onDelete={handleDelete} />
この場合、user.name が undefined となり、画面に何も表示されません。さらに悪いことに、エディタでは何も警告が出ず、実行して初めて気づくことになります。
Store の型不明による予期しない状態更新
Svelte の Store は、writable や readable といった仕組みでグローバルな状態管理を実現します。しかし、型定義がないと Store に何を格納できるかが不明確になり、意図しないデータ構造の変更が発生します。
実際に遭遇した問題として、以下のようなケースがありました。
javascript// stores.js (型なし)
import { writable } from "svelte/store";
export const userStore = writable({
id: null,
name: "",
isLoggedIn: false,
});
javascript// ComponentA.svelte
<script>
import { userStore } from './stores.js';
userStore.set({
id: 123,
name: 'Taro',
isLoggedIn: true,
role: 'admin' // ❌ 後から追加したプロパティ
});
</script>
javascript// ComponentB.svelte
<script>
import { userStore } from './stores.js';
$: if ($userStore.permissions) { // ❌ permissions は存在しない
console.log('Permissions:', $userStore.permissions);
}
</script>
Store の構造が統一されておらず、各コンポーネントで異なるプロパティを期待してしまう問題が発生しました。放置すると、「ログイン状態なのに権限チェックが動かない」といった、デバッグが困難なバグにつながります。
API レスポンスの構造変更への対応遅れ
API 連携では、バックエンドのレスポンス構造が変更されることがあります。型定義がないと、この変更に気づくのが遅れ、本番環境でエラーが発生するリスクがあります。
業務での実例として、以下のような変更がありました。
変更前のレスポンス
json{
"user": {
"id": 1,
"name": "Taro",
"email": "taro@example.com"
}
}
変更後のレスポンス
json{
"data": {
"user": {
"id": 1,
"fullName": "Taro Yamada",
"email": "taro@example.com"
}
}
}
この変更により、以下のコードが動かなくなりました。
javascript// 型なしのコード
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return data.user; // ❌ data.data.user に変更されている
}
TypeScript で型定義していれば、ビルド時に data.user が存在しないことが検出され、修正が促されます。しかし型なしでは、ユーザーがアクセスして初めてエラーが発覚するという最悪のシナリオになります。
mermaidflowchart TD
api["API 変更"] --> deploy["バックエンド<br/>デプロイ"]
deploy --> frontend["フロントエンド<br/>アクセス"]
frontend --> check{"型チェック<br/>あり?"}
check -->|なし| runtime["ランタイム<br/>エラー"]
check -->|あり| build["ビルドエラー<br/>で検出"]
runtime --> user["ユーザーに<br/>影響"]
build --> fix["修正後<br/>デプロイ"]
この章でわかること
- Props の型不一致がランタイムエラーを引き起こす
- Store の型がないと状態管理が破綻しやすい
- API 変更への対応が遅れるリスク
つまずきポイント
- 型なしでは「動いているように見えて実は壊れている」状態が発生しやすい
- 小規模プロジェクトでは問題が顕在化しにくく、規模が大きくなってから気づく
TypeScript による型安全な開発スタイルの選択
SvelteKit プロジェクトでの TypeScript 導入判断
実際に TypeScript を導入する際、「新規プロジェクトから始める」か「既存プロジェクトに途中から導入する」かで戦略が変わります。
業務で検証した結果、以下の判断基準を採用しました。
| # | プロジェクト状況 | 推奨アプローチ | 理由 |
|---|---|---|---|
| 1 | 新規プロジェクト | 最初から TypeScript | 後から追加するコストが高い |
| 2 | 小規模既存 (10 ファイル未満) | 一気に TypeScript 化 | 影響範囲が小さく短期間で完了 |
| 3 | 中規模既存 (50 ファイル未満) | 段階的に TypeScript 化 | 重要な部分から順に型を追加 |
| 4 | 大規模既存 (100 ファイル以上) | 新規コードのみ TypeScript | 全体移行はコスト高、新規から徐々に |
新規プロジェクトでの TypeScript 有効化
SvelteKit では、プロジェクト作成時に TypeScript を選択するだけで環境が整います。
bash# SvelteKit プロジェクトの作成
npm create svelte@latest my-app
cd my-app
npm install
対話形式で以下を選択します。
- Which Svelte app template?: SvelteKit demo app (推奨)
- Add type checking with TypeScript?: Yes, using TypeScript syntax
- Select additional options: Prettier, ESLint, Vitest (お好みで)
既存プロジェクトへの TypeScript 追加
既存の JavaScript プロジェクトに TypeScript を追加する場合、公式の移行ツールを使用します。
bashnpx sv migrate typescript
npm install
このコマンドにより、以下が自動生成されます。
tsconfig.json: TypeScript 設定src/app.d.ts: SvelteKit 用の型定義vite.config.ts: Vite 設定の TypeScript 化
実際に試したところ、このコマンド一発で環境構築が完了し、既存の .svelte ファイルに <script lang="ts"> を追加するだけで TypeScript が有効になりました。
tsconfig.json の推奨設定
自動生成される tsconfig.json は以下のようになります。
json{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
}
重要な設定項目の説明
strict: true(最重要)
厳格な型チェックを有効化します。以下のオプションがまとめて有効になります。
noImplicitAny: 型推論できない場合にanyを禁止strictNullChecks:nullとundefinedを厳密にチェックstrictFunctionTypes: 関数の引数と戻り値を厳密にチェック
業務では strict: true を必須としています。これがないと TypeScript を導入した意味が薄れます。
allowJs / checkJs
JavaScript ファイルと TypeScript ファイルの混在を許可します。段階的に移行する際に有用です。
allowJs: true:.jsファイルのインポートを許可checkJs: true:.jsファイルも型チェック対象にする
採用しなかった選択肢として、checkJs: false にすることも可能ですが、これだと JavaScript ファイルの型エラーに気づけません。段階的移行であっても checkJs: true を推奨します。
この章でわかること
- プロジェクト規模に応じた TypeScript 導入戦略
- 公式ツールで簡単に環境構築できる
- strict モードは必須
つまずきポイント
strict: trueにすると既存コードでエラーが大量に出る場合がある- 段階的移行では
allowJsとcheckJsの設定を理解しないと混乱する
ユースケース別の型安全実装パターン
Props の型付けパターン
Svelte コンポーネントで Props を受け取る際、TypeScript でインターフェースを定義することで、親コンポーネントから渡される値の型を保証できます。
基本的な Props の型定義
シンプルなプリミティブ型の Props は、以下のように定義します。
typescript<!-- UserCard.svelte -->
<script lang="ts">
export let name: string;
export let age: number;
export let email: string | undefined = undefined; // オプショナル
export let isActive: boolean = true; // デフォルト値付き
</script>
<div class="user-card">
<h3>{name}</h3>
<p>年齢: {age}</p>
{#if email}
<p>メール: {email}</p>
{/if}
<span class:active={isActive}>
{isActive ? 'アクティブ' : '非アクティブ'}
</span>
</div>
この定義により、親コンポーネントで以下のように使用する際、型チェックが働きます。
typescript<!-- App.svelte -->
<script lang="ts">
import UserCard from './UserCard.svelte';
</script>
<UserCard name="太郎" age={25} email="taro@example.com" />
<UserCard name="花子" age={30} /> <!-- email は省略可能 -->
<UserCard name="次郎" age="25" /> <!-- ❌ age は number 型が必要 -->
インターフェースを使った複雑な Props
オブジェクト型の Props は、インターフェースとして定義すると再利用性と可読性が向上します。
typescript// src/lib/types/user.ts
export interface User {
id: number;
name: string;
email: string;
avatar?: string; // オプショナルプロパティ
createdAt: Date;
}
export interface UserRole {
role: "admin" | "editor" | "viewer"; // ユニオン型で限定
permissions: string[];
}
typescript<!-- UserProfile.svelte -->
<script lang="ts">
import type { User, UserRole } from '$lib/types/user';
export let user: User;
export let role: UserRole;
export let onEdit: (user: User) => void; // 関数の型も定義
</script>
<div class="profile">
<img src={user.avatar ?? '/default-avatar.png'} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.email}</p>
<p>役割: {role.role}</p>
<button on:click={() => onEdit(user)}>編集</button>
</div>
実際に試したところ、インターフェースを分離することで、以下のメリットがありました。
- 複数のコンポーネントで同じ型を再利用できる
- 型定義ファイルを見るだけでデータ構造が理解できる
- API レスポンスの型と統一できる
ジェネリクスを使った汎用コンポーネント
Svelte 5 では、ジェネリクスを使った型安全な汎用コンポーネントが作成できます。
typescript<!-- DataList.svelte -->
<script lang="ts" generics="T">
export let items: T[];
export let getId: (item: T) => string | number;
export let renderItem: (item: T) => string;
export let onSelect: ((item: T) => void) | undefined = undefined;
</script>
<ul class="data-list">
{#each items as item (getId(item))}
<li>
<button on:click={() => onSelect?.(item)}>
{renderItem(item)}
</button>
</li>
{/each}
</ul>
使用例:
typescript<script lang="ts">
import DataList from './DataList.svelte';
interface Product {
id: number;
name: string;
price: number;
}
let products: Product[] = [
{ id: 1, name: 'ノートPC', price: 89800 },
{ id: 2, name: 'マウス', price: 2980 }
];
function handleSelect(product: Product) {
console.log('Selected:', product.name);
}
</script>
<DataList
items={products}
getId={(p) => p.id}
renderItem={(p) => `${p.name} - ¥${p.price.toLocaleString()}`}
onSelect={handleSelect}
/>
ジェネリクスを使うことで、任意の型の配列を扱える汎用的なコンポーネントが作成でき、型安全性も保たれます。
状態管理 (Store) の型安全パターン
Svelte の Store を使ったグローバル状態管理では、Store の型を明示的に定義することで、状態の一貫性を保証できます。
Writable Store の型定義
基本的な Writable Store の型定義方法です。
typescript// src/lib/stores/counter.ts
import { writable, type Writable } from "svelte/store";
export const count: Writable<number> = writable(0);
// 型安全なヘルパー関数
export function increment() {
count.update((n) => n + 1);
}
export function decrement() {
count.update((n) => n - 1);
}
export function reset() {
count.set(0);
}
コンポーネントでの使用:
typescript<script lang="ts">
import { count, increment, decrement, reset } from '$lib/stores/counter';
</script>
<div>
<p>カウント: {$count}</p>
<button on:click={increment}>+1</button>
<button on:click={decrement}>-1</button>
<button on:click={reset}>リセット</button>
</div>
複雑な状態の型定義
実務では、Store に複雑なオブジェクトを格納することが多いです。
typescript// src/lib/stores/auth.ts
import { writable, derived, type Readable } from "svelte/store";
export interface User {
id: number;
name: string;
email: string;
role: "admin" | "user" | "guest";
}
export interface AuthState {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
}
const initialState: AuthState = {
user: null,
isAuthenticated: false,
isLoading: false,
error: null,
};
export const authStore = writable<AuthState>(initialState);
// 派生 Store の型定義
export const currentUser: Readable<User | null> = derived(
authStore,
($auth) => $auth.user,
);
export const isAdmin: Readable<boolean> = derived(
authStore,
($auth) => $auth.user?.role === "admin",
);
// 型安全な認証アクション
export async function login(email: string, password: string): Promise<void> {
authStore.update((state) => ({ ...state, isLoading: true, error: null }));
try {
const response = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (!response.ok) throw new Error("ログインに失敗しました");
const user: User = await response.json();
authStore.set({
user,
isAuthenticated: true,
isLoading: false,
error: null,
});
} catch (error) {
authStore.update((state) => ({
...state,
isLoading: false,
error: error instanceof Error ? error.message : "不明なエラー",
}));
}
}
export function logout(): void {
authStore.set(initialState);
}
業務で実際に使用した結果、以下の点が重要だとわかりました。
- Store の初期値を
initialStateとして明示的に定義する - 派生 Store を使って、必要な値だけを公開する
- Store を直接操作せず、関数経由でアクセスさせる
mermaidstateDiagram-v2
[*] --> NotAuthenticated
NotAuthenticated --> Loading : login() 実行
Loading --> Authenticated : ログイン成功
Loading --> Error : ログイン失敗
Error --> NotAuthenticated : リトライ
Authenticated --> NotAuthenticated : logout() 実行
state Authenticated {
[*] --> UserRole
UserRole --> Admin : role = admin
UserRole --> User : role = user
}
Custom Store パターン
Store をカプセル化して、外部から直接操作できないようにするパターンです。
typescript// src/lib/stores/todos.ts
import { writable } from "svelte/store";
export interface Todo {
id: number;
text: string;
completed: boolean;
createdAt: Date;
}
function createTodoStore() {
const { subscribe, set, update } = writable<Todo[]>([]);
return {
subscribe, // 読み取りのみ公開
add: (text: string) => {
const newTodo: Todo = {
id: Date.now(),
text,
completed: false,
createdAt: new Date(),
};
update((todos) => [...todos, newTodo]);
},
toggle: (id: number) => {
update((todos) =>
todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo,
),
);
},
remove: (id: number) => {
update((todos) => todos.filter((todo) => todo.id !== id));
},
clear: () => set([]),
};
}
export const todos = createTodoStore();
このパターンを採用した理由:
- Store の操作を関数に限定し、予期しない更新を防ぐ
- インターフェースが明確で、使い方がわかりやすい
- 型安全性を保ちながら、カプセル化も実現
この章でわかること
- Props はインターフェースで型定義すると再利用性が高まる
- Store は型パラメータで状態の一貫性を保証する
- Custom Store パターンで操作を制限できる
つまずきポイント
- ジェネリクスは便利だが、初学者には理解が難しい
- Store の型定義を後から変更すると、全体への影響が大きい
イベントの型安全パターン
Svelte のコンポーネント間通信では、createEventDispatcher を使ってカスタムイベントを発火します。TypeScript でイベントペイロードの型を定義することで、型安全なイベント送受信が可能になります。
createEventDispatcher の型定義
イベントの型を定義するには、イベント名とペイロードの型をマッピングしたインターフェースを作成します。
typescript<!-- FormComponent.svelte -->
<script lang="ts">
import { createEventDispatcher } from 'svelte';
// イベントペイロードの型定義
interface FormEvents {
submit: { name: string; email: string };
cancel: { reason: string };
validate: { field: string; isValid: boolean };
}
const dispatch = createEventDispatcher<FormEvents>();
let name = '';
let email = '';
function handleSubmit() {
// 型安全なイベント発火
dispatch('submit', { name, email });
}
function handleCancel() {
dispatch('cancel', { reason: 'user-action' });
}
</script>
<form on:submit|preventDefault={handleSubmit}>
<input bind:value={name} placeholder="名前" />
<input bind:value={email} type="email" placeholder="メール" />
<button type="submit">送信</button>
<button type="button" on:click={handleCancel}>キャンセル</button>
</form>
親コンポーネントでのイベント受信:
typescript<!-- App.svelte -->
<script lang="ts">
import FormComponent from './FormComponent.svelte';
function handleSubmit(event: CustomEvent<{ name: string; email: string }>) {
const { name, email } = event.detail;
console.log('送信:', name, email);
}
function handleCancel(event: CustomEvent<{ reason: string }>) {
console.log('キャンセル理由:', event.detail.reason);
}
</script>
<FormComponent on:submit={handleSubmit} on:cancel={handleCancel} />
実際に試したところ、イベントペイロードの型が明確になることで、以下のメリットがありました。
- エディタの自動補完が効く
- ペイロードの構造が変わったときにコンパイルエラーで気づける
- ドキュメントを見なくてもイベントの仕様がわかる
イベント型の共通化
複数のコンポーネントで同じイベント型を使う場合、型定義ファイルに切り出します。
typescript// src/lib/types/events.ts
export interface UserActionEvents {
create: { user: { name: string; email: string } };
update: { userId: number; changes: Partial<{ name: string; email: string }> };
delete: { userId: number; confirmed: boolean };
}
export interface NotificationEvents {
show: { message: string; type: "info" | "warning" | "error" };
hide: { notificationId: string };
}
使用例:
typescript<script lang="ts">
import { createEventDispatcher } from 'svelte';
import type { UserActionEvents } from '$lib/types/events';
const dispatch = createEventDispatcher<UserActionEvents>();
function deleteUser(userId: number) {
if (confirm('本当に削除しますか?')) {
dispatch('delete', { userId, confirmed: true });
}
}
</script>
API データの型管理パターン
API 連携では、レスポンスの型を定義することで、データ構造の変更に強い実装が可能になります。
API レスポンスの型定義
まず、API から返されるデータの型を定義します。
typescript// src/lib/types/api.ts
export interface ApiResponse<T> {
data: T;
status: "success" | "error";
message?: string;
}
export interface User {
id: number;
name: string;
email: string;
avatar?: string;
role: "admin" | "user";
createdAt: string; // ISO 8601 文字列
}
export interface ApiError {
error: {
code: string;
message: string;
details?: Record<string, string[]>;
};
}
型安全な fetch ラッパー関数
API クライアントを作成し、型安全な通信を実現します。
typescript// src/lib/api/client.ts
import type { ApiResponse, ApiError } from "$lib/types/api";
export class ApiClient {
private baseUrl: string;
constructor(baseUrl = "/api") {
this.baseUrl = baseUrl;
}
private async request<T>(
endpoint: string,
options?: RequestInit,
): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`, options);
if (!response.ok) {
const error: ApiError = await response.json();
throw new Error(error.error.message);
}
return response.json();
}
async get<T>(endpoint: string): Promise<ApiResponse<T>> {
return this.request<ApiResponse<T>>(endpoint);
}
async post<T, U = unknown>(
endpoint: string,
data: U,
): Promise<ApiResponse<T>> {
return this.request<ApiResponse<T>>(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
}
async put<T, U = unknown>(
endpoint: string,
data: U,
): Promise<ApiResponse<T>> {
return this.request<ApiResponse<T>>(endpoint, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
}
async delete<T>(endpoint: string): Promise<ApiResponse<T>> {
return this.request<ApiResponse<T>>(endpoint, { method: "DELETE" });
}
}
export const apiClient = new ApiClient();
SvelteKit の load 関数での型安全なデータフェッチ
SvelteKit の load 関数内で API データを取得する際も、型安全性を保ちます。
typescript// src/routes/users/+page.ts
import type { PageLoad } from "./$types";
import type { User } from "$lib/types/api";
import { apiClient } from "$lib/api/client";
export const load: PageLoad = async () => {
try {
const response = await apiClient.get<User[]>("/users");
return {
users: response.data,
status: response.status,
};
} catch (error) {
return {
users: [],
error: error instanceof Error ? error.message : "不明なエラー",
};
}
};
コンポーネントでのデータ使用:
typescript<!-- src/routes/users/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
$: users = data.users ?? [];
$: error = data.error;
</script>
{#if error}
<p class="error">{error}</p>
{:else}
<ul>
{#each users as user (user.id)}
<li>
<strong>{user.name}</strong> ({user.email})
<span class="role">{user.role}</span>
</li>
{/each}
</ul>
{/if}
業務で実際に使用した結果、API の型定義により以下の効果がありました。
- バックエンドの API 仕様変更に即座に気づける
- レスポンスの構造が明確で、データの取り扱いに迷わない
- エラーハンドリングが統一され、保守しやすい
mermaidsequenceDiagram
participant Page as +page.svelte
participant Load as load 関数
participant Client as ApiClient
participant API as バックエンド API
participant Type as TypeScript<br/>コンパイラ
Page->>Load: ページ読み込み
Load->>Client: get<User[]>('/users')
Client->>API: fetch リクエスト
API->>Client: JSON レスポンス
Client->>Type: 型チェック
Type-->>Client: ApiResponse<User[]>
Client->>Load: 型安全なデータ
Load->>Page: PageData として渡す
Page->>Page: 型安全に表示
Zod による実行時型検証
TypeScript のコンパイル時チェックに加え、実行時にも型を検証する場合、Zod ライブラリを使用します。
typescript// src/lib/types/api.ts
import { z } from "zod";
export const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
avatar: z.string().url().optional(),
role: z.enum(["admin", "user"]),
createdAt: z.string().datetime(),
});
export type User = z.infer<typeof UserSchema>;
export const ApiResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
z.object({
data: dataSchema,
status: z.enum(["success", "error"]),
message: z.string().optional(),
});
typescript// src/lib/api/client.ts
import { UserSchema, ApiResponseSchema } from "$lib/types/api";
export async function fetchUsers(): Promise<User[]> {
const response = await fetch("/api/users");
const json = await response.json();
// 実行時型検証
const parsed = ApiResponseSchema(z.array(UserSchema)).parse(json);
return parsed.data;
}
Zod を採用した理由:
- API レスポンスが予期しない構造だった場合に、実行時にエラーを検出できる
- TypeScript の型定義とスキーマを統一できる
採用しなかった選択肢として、any にキャストして型検証をスキップすることも可能ですが、実行時のバグを見逃すリスクが高まるため推奨しません。
この章でわかること
- イベントペイロードの型定義で、コンポーネント間通信が安全になる
- API レスポンスの型定義で、データ構造の変更に強くなる
- Zod で実行時型検証を追加できる
つまずきポイント
- CustomEvent の型定義が冗長になりやすい
- API の型定義とバックエンドの仕様が乖離すると、実行時エラーになる
型安全開発スタイルの実務判断まとめ
ここまで解説したユースケース別の型安全パターンを、実務での判断基準と合わせて整理します。
ユースケース別の型付け優先度
実際のプロジェクトでは、すべてのコードに厳密な型を付けるのは現実的ではありません。以下の優先度で段階的に型安全化を進めることを推奨します。
| 優先度 | ユースケース | 型付けの範囲 | 理由 |
|---|---|---|---|
| 最優先 | API レスポンス | すべてのエンドポイント | データ構造の変更に気づかないと致命的 |
| 最優先 | Store (グローバル状態) | すべての Store | 状態管理の破綻を防ぐ |
| 高 | コンポーネント Props | 公開 API となるコンポーネント | 再利用時の誤用を防ぐ |
| 中 | イベントペイロード | 複雑なデータを送信するイベント | シンプルなイベントは省略可 |
| 低 | ローカル変数 | 型推論で不十分な箇所のみ | TypeScript の型推論に任せる |
型安全と開発速度のトレードオフ
TypeScript を導入すると、初期の開発速度は若干低下します。しかし、中長期的には以下の理由で開発効率が向上します。
mermaidflowchart LR
start["プロジェクト<br/>開始"] --> phase1["初期開発<br/>フェーズ"]
phase1 --> without["型なし開発"]
phase1 --> with["型あり開発"]
without --> fast1["初期は速い"]
fast1 --> slow1["バグ対応で<br/>速度低下"]
with --> slow2["初期は遅い"]
slow2 --> fast2["バグ少なく<br/>速度維持"]
slow1 --> result["長期的には<br/>型ありが効率的"]
fast2 --> result
業務での検証結果:
- 1〜2 週間のプロトタイプ: 型なしの方が速い
- 1〜3 ヶ月の小規模プロジェクト: 型ありの方が安定する
- 6 ヶ月以上の中長期プロジェクト: 型ありが圧倒的に有利
採用した設計判断と採用しなかった選択肢
実務で TypeScript を導入する際、以下の判断を行いました。
strict モードは必須
採用: tsconfig.json で strict: true を設定
理由:
- 型安全性を最大限に活用するため
- 後から strict にすると修正コストが高い
採用しなかった選択肢: strict: false でゆるい型チェック
理由: TypeScript を導入する意味が薄れる
any は原則禁止、unknown を使用
採用: 型が不明な場合は unknown を使用し、型ガードで絞り込む
typescriptfunction processData(data: unknown) {
if (typeof data === "string") {
console.log(data.toUpperCase()); // string と推論される
} else if (Array.isArray(data)) {
console.log(data.length); // array と推論される
}
}
採用しなかった選択肢: any で型チェックをスキップ
理由: any は型安全性を完全に失うため
コンポーネントの Props は必ず型定義
採用: すべての公開コンポーネントで Props の型を定義
理由:
- コンポーネントの再利用時に誤用を防ぐ
- ドキュメントとしても機能する
採用しなかった選択肢: 内部コンポーネントは型定義を省略
理由: 後から公開コンポーネントに変わることがあり、後付けが面倒
API の型定義とスキーマ検証を併用
採用: TypeScript の型定義 + Zod での実行時検証
理由:
- コンパイル時と実行時の二重チェックで安全性が高まる
- バックエンドの仕様変更に気づきやすい
採用しなかった選択肢: TypeScript の型定義のみ
理由: 実行時のデータが型定義と一致する保証がない
初学者と実務者への推奨アプローチ
初学者向けの学習順序
- 基本的な型から始める:
string,number,boolean,array - Props の型定義を練習: 単純なコンポーネントから
- Store の型定義: グローバル状態管理の基礎
- API の型定義: 実践的なアプリケーション開発
つまずきやすいポイント:
- ジェネリクスは最初は飛ばしてもよい
unknownとanyの違いは後回しでもよい- まずは「型を付ける習慣」を身につける
実務者向けの導入戦略
- 新規コンポーネントから TypeScript 化: 既存は後回し
- API レスポンスの型定義を優先: 最も効果が高い
- Store の型定義: 状態管理の安全性を確保
- 既存コードの段階的移行: 優先度の高い部分から
業務で問題になったケース:
- すべてのコードを一気に TypeScript 化しようとして挫折
anyで逃げすぎて、型安全性が保たれない
解決策:
- 優先度を決めて段階的に移行
anyを使う場合は、コメントで理由を残す
この章でわかること
- 型付けの優先度を決めて段階的に導入すると効率的
- strict モードは必須、any は原則禁止
- 初学者は基本から、実務者は新規コードから TypeScript 化
つまずきポイント
- 完璧主義に陥ると開発が進まない
- チーム全員の TypeScript スキルレベルに差があると混乱する
まとめ
Svelte と TypeScript を組み合わせた型安全な開発スタイルは、Props、状態管理、イベント、API 連携といったユースケースごとに適切な型付けパターンが存在します。重要なのは、すべてに厳密な型を付けるのではなく、優先度を決めて段階的に導入することです。
本記事で解説した内容を振り返ります。
型安全開発で得られる効果
- API レスポンスの型定義: データ構造の変更に即座に気づき、本番環境でのエラーを防ぐ
- Store の型定義: グローバル状態の一貫性を保ち、予期しない状態更新を防ぐ
- Props の型定義: コンポーネントの再利用時に誤用を防ぎ、保守性を向上させる
- イベントの型定義: コンポーネント間通信を明確にし、ペイロードの構造を保証する
実務での判断基準
静的型付けを導入する際は、以下の基準で優先度を決めることを推奨します。
| 判断基準 | 推奨アプローチ | 条件 |
|---|---|---|
| プロジェクト規模 | 新規は TypeScript 必須 | 中長期運用が前提 |
| 既存プロジェクト | API と Store から型定義 | 段階的移行が現実的 |
| チームスキル | 基本的な型から始める | ジェネリクスは後回し可 |
| 開発速度 | strict モードは必須 | 初期は遅くても長期では効率的 |
型安全と UI/UX の関係
型安全性は開発者体験だけでなく、エンドユーザーの UI/UX にも影響します。API の型定義により、データ構造の変更をビルド時に検出できれば、ユーザーがエラー画面を見る確率が大幅に減少します。
段階的な導入が成功の鍵
業務で検証した結果、一気にすべてを TypeScript 化するのではなく、以下の順序で進めることが効果的でした。
- 新規コンポーネントから TypeScript を採用
- API レスポンスと Store の型定義を優先
- 既存コードは影響範囲を見ながら段階的に移行
anyを使う場合はコメントで理由を残す
TypeScript の学習コストは存在しますが、中長期的な開発効率と品質向上を考慮すると、投資価値は非常に高いものになります。Svelte の軽量性と TypeScript の安全性を組み合わせることで、保守しやすく、スケーラブルなインターフェース設計が可能になるでしょう。
関連リンク
著書
article2025年12月29日SvelteとTypeScriptのユースケース 型安全な開発スタイルを整理する
articleSvelte 可観測性の実装:Sentry/OpenTelemetry/Web Vitals 連携ガイド
articleSvelte URL ドリブン設計:検索パラメータとストアの同期パターン
articleSvelte ストア速見表:writable/derived/readable/custom の実用スニペット
articlesvelte-preprocess 導入ガイド:SCSS・PostCSS・TypeScript を安全に共存
articleSvelteKit アダプタ比較:Node/Vercel/Cloudflare/Netlify の速度と制約
article2026年1月13日TypeScriptで既存コードを型安全化する使い方 段階的リファクタリング手順とチェックポイント
article2026年1月13日PlaywrightとTypeScriptでテスト自動化を運用する 型安全な設計と保守の要点
article2026年1月13日TypeScriptでHigher Kinded Typesを模倣する設計 ジェネリクスで関数型パターンを整理
article2026年1月13日Viteで画像とアセット管理をシンプルにする使い方 import運用と構成の考え方
article2026年1月13日TypeScriptで型レベル計算の使い方を学ぶ 算術演算を型システムで実装する
article2026年1月13日TypeScriptで実行時バリデーション自動生成を設計する 型と実行時チェックを整合させる
articleNode.jsセキュリティアップデート、今すぐ必要?環境別の判断フローチャート
articleNode.js HTTP/2サーバーが1リクエストでダウン:CVE-2025-59465の攻撃手法と防御策
articleDatadog・New Relic利用者は要注意:async_hooksの脆弱性がAPMツール経由でDoSを引き起こす理由
articleNext.js・React Server Componentsが危険?async_hooksの脆弱性CVE-2025-59466を徹底解説
article【緊急】2026年1月13日発表 Node.js 脆弱性8件の詳細と対策|HTTP/2・async_hooks のDoS問題を解説
article2026年1月13日TypeScriptで既存コードを型安全化する使い方 段階的リファクタリング手順とチェックポイント
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
