Svelte 5の$props()の型定義と$bindableを整理した話

Svelte 5で $props() にTypeScriptの型を付ける方法と、子コンポーネントから親の値を変更できる $bindable の使い方を整理します。


$props() に型を付ける

type Props を定義して $props() の分割代入に型注釈を付けます。

子コンポーネント(Counter.svelte)

<!-- src/lib/components/Counter.svelte -->
<script lang="ts">
  type Props = {
    count: number;
    onIncrement: () => void;
    onDecrement: () => void;
  };

  let { count, onIncrement, onDecrement }: Props = $props();
</script>

<div>
  <p>カウント: {count}</p>
  <button onclick={onIncrement}>+1</button>
  <button onclick={onDecrement}>-1</button>
</div>

子コンポーネント(Display.svelte)

<!-- src/lib/components/Display.svelte -->
<script lang="ts">
  type Props = {
    count: number;
  };

  let { count }: Props = $props();
</script>

<div>
  <p>Displayコンポーネントから見たカウント: {count}</p>
</div>

型を付けることで、親コンポーネントが必要なpropsを渡し忘れたときや型が合わないときにエラーになります。


storeを使う場合の型

storeを使う場合は TypeScript の型推論が自動的に働くため、propsほど意識しなくて済みます。

// src/lib/store.svelte.ts
function createCountStore() {
  let count = $state(0);  // TypeScript が number と推論

  return {
    get count() { return count; },  // 戻り値も number と推論
    increment() { count++; },
    decrement() { count--; },
  };
}

export const countStore = createCountStore();
// countStore の型は自動的に推論される:
// { readonly count: number; increment(): void; decrement(): void; }

コンポーネント側では countStore.count を使うだけで型チェックが効きます。

複雑な型は明示的に書く

オブジェクトや配列など複雑な状態のときは $state に型引数を渡すと安全です。

type User = {
  id: string;
  name: string;
  email: string;
};

function createUserStore() {
  let user = $state<User | null>(null);  // null か User

  return {
    get user() { return user; },
    setUser(newUser: User) { user = newUser; },
    clear() { user = null; },
  };
}

propsとstoreの型の書き方の違い

props store
型の書き方 type Props = {...} を明示 $state<型>() で指定、あとは推論
なぜ違う $props() は外から渡されるため推論できない $state の型から自動的に追跡される

propsは「外から渡される値」なので何が来るか宣言が必要です。storeは「自分で定義した値」なのでTypeScriptが内部の型を追跡できます。


$bindable — 子コンポーネントから親の値を変更する

通常のpropsは親 → 子の一方向です。$bindable を使うと子コンポーネントが親の値を直接変更できる双方向バインディングになります。

通常のprops:  親 → 子(一方向)
$bindable:   親 ⇄ 子(双方向)

子コンポーネント(TextInput.svelte)

<!-- src/lib/components/TextInput.svelte -->
<script lang="ts">
  type Props = {
    value?: string;  // ? をつけて省略可能にする
  };

  let { value = $bindable('デフォルトテキスト') }: Props = $props();
</script>

<input type="text" bind:value={value} />

$bindable()$props() の中でデフォルト値として使います。value?: string と省略可能にすることで、バインドせずに呼び出すこともできます。

親コンポーネントで使う

<!-- src/routes/learn/+page.svelte -->
<script lang="ts">
  import TextInput from '$lib/components/TextInput.svelte';

  let text = $state('');
</script>

<!-- bind:value で双方向バインディング -->
<TextInput bind:value={text} />
<p>入力された値: {text}</p>

bind:value={text} と書くことで、子コンポーネント内で value を変更すると親の text も自動的に更新されます。

デフォルト値の反映のされ方

<!-- bind:value あり → 親の '' が使われる(デフォルト値は無視) -->
<TextInput bind:value={text} />

<!-- bind:value なし → 'デフォルトテキスト' が使われる -->
<TextInput />
呼び方 使われる値
bind:value={text} あり 親の $state の値
bind:value なし $bindable('デフォルトテキスト') の値

汎用コンポーネントとして「バインドしなくても単体で動く」ようにしたいときにデフォルト値を指定します。バインドして使うのが前提なら $bindable() のままで十分です。

$bindable を使うべきケース

ケース 使うべきか
フォームの入力値(input・textarea) ✅ よく使う
チェックボックスのon/off ✅ よく使う
複雑なロジックを伴う状態変更 ❌ コールバック(onXxx)の方が明示的
複数コンポーネントで共有する状態 ❌ storeの方が適切

まとめ

内容
type Props $props() に型を付けるための型エイリアス
storeの型 $state<型>() で指定、戻り値は自動推論
$bindable() 双方向バインディング。子から親の値を変更できる
value?: string ? を付けて省略可能にする。バインドなしで呼べるようになる
デフォルト値 $bindable('初期値') でバインドなし時の値を指定
← トップページに戻る