TypeScript統合

SvelteはTypeScriptを第一級市民として扱い、優れた型安全性とIDEサポートを提供します。このページでは、SvelteコンポーネントでTypeScriptを効果的に使用する方法を学びます。

TypeScriptの有効化

SvelteでTypeScriptを使用するのは非常に簡単です。特別な設定はほとんど必要なく、lang="ts"属性を追加するだけで始められます。

1. script タグでTypeScriptを使用

SvelteコンポーネントでTypeScriptを有効にする最も基本的な方法は、<script>タグにlang="ts"属性を追加することです。

<!-- lang="ts"を追加するだけ -->
<script lang="ts">
  let message: string = 'Hello TypeScript!';
  let count: number = 0;
  let isActive: boolean = true;
</script>
svelte

2. プロジェクト全体の設定

SvelteKitプロジェクトではTypeScriptがデフォルトでサポートされています。svelte.config.jsでViteのプリプロセッサーが自動的にTypeScriptを処理します。

import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

export default {
  preprocess: vitePreprocess() // TypeScriptを自動処理
};
javascript

Props の型定義

TypeScriptを使用することで、コンポーネントのPropsに明確な型定義を与えることができます。これにより、コンポーネント間のデータの受け渡しが型安全になり、開発時のミスを防げます。

基本的なProps

従来のexport let構文に型注釈を追加することで、Propsの型を定義できます。

<script lang="ts">
  // 必須のprop
  export let name: string;
  
  // オプショナルなprop(デフォルト値あり)
  export let age: number = 0;
  
  // オプショナルなprop(undefined可能)
  export let email: string | undefined = undefined;
  
  // 複雑な型
  export let user: {
    id: number;
    name: string;
    roles: string[];
  };
</script>
svelte

インターフェースを使用したProps

複雑なPropsの構造を扱う場合、インターフェースを定義することで、より組織的で再利用可能な型定義を作成できます。

<script lang="ts">
  interface UserData {
    id: number;
    name: string;
    email: string;
    createdAt: Date;
  }
  
  interface Props {
    title: string;
    user: UserData;
    onSave?: (user: UserData) => void;
  }
  
  // 個別にエクスポート
  export let title: Props['title'];
  export let user: Props['user'];
  export let onSave: Props['onSave'] = undefined;
</script>
svelte

イベントの型定義

Svelteでイベントハンドラを記述する際、TypeScriptの型システムを活用することで、イベントオブジェクトのプロパティに安全にアクセスできます。

DOM イベント

標準的なDOMイベントには、TypeScriptの組み込み型定義を使用します。currentTargetの型アサーションを使うことで、要素固有のプロパティにアクセスできます。

<script lang="ts">
  function handleClick(event: MouseEvent): void {
    const target = event.currentTarget as HTMLButtonElement;
    console.log('Button clicked:', target.textContent);
  }
  
  function handleInput(event: Event): void {
    const target = event.currentTarget as HTMLInputElement;
    console.log('Input value:', target.value);
  }
  
  function handleKeydown(event: KeyboardEvent): void {
    if (event.key === 'Enter') {
      console.log('Enter pressed');
    }
  }
  
  function handleSubmit(event: SubmitEvent): void {
    event.preventDefault();
    const formData = new FormData(event.currentTarget as HTMLFormElement);
    // フォーム処理
  }
</script>

<button on:click={handleClick}>クリック</button>
<input on:input={handleInput} on:keydown={handleKeydown} />
<form on:submit={handleSubmit}>
  <!-- フォーム要素 -->
</form>
svelte

カスタムイベント

SvelteのcreateEventDispatcherを使用する際、ジェネリクスを使ってカスタムイベントの型を定義できます。これにより、イベント名とペイロードの型が厳密にチェックされます。

<script lang="ts">
  import { createEventDispatcher } from 'svelte';
  
  // イベントの型定義
  interface EventMap {
    save: { id: number; data: string };
    delete: { id: number };
    update: { field: string; value: any };
  }
  
  const dispatch = createEventDispatcher<EventMap>();
  
  function handleSave(): void {
    // 型安全なイベント発火
    dispatch('save', { id: 1, data: 'test' });
    
    // TypeScriptエラー:型が合わない
    // dispatch('save', { id: 'string' }); // Error!
  }
</script>
svelte

ジェネリック型の使用

Svelte 5では、コンポーネント自体をジェネリックにすることができます。これにより、異なる型のデータを扱える汎用的なコンポーネントを作成できます。

ジェネリックコンポーネント

<script>タグにgenerics属性を追加することで、型パラメーターを定義できます。

<!-- List.svelte -->
<script lang="ts" generics="T">
  export let items: T[];
  export let getKey: (item: T) => string | number;
  export let renderItem: (item: T) => string;
  export let onSelect: ((item: T) => void) | undefined = undefined;
</script>

{#each items as item (getKey(item))}
  <div on:click={() => onSelect?.(item)}>
    {@html renderItem(item)}
  </div>
{/each}
svelte

使用例

上記のジェネリックコンポーネントを使用する際、TypeScriptは自動的に型を推論し、型安全性を保証します。

<script lang="ts">
  import List from './List.svelte';
  
  interface User {
    id: number;
    name: string;
  }
  
  let users: User[] = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' }
  ];
</script>

<List
  items={users}
  getKey={(user) => user.id}
  renderItem={(user) => `<strong>${user.name}</strong>`}
  onSelect={(user) => console.log('Selected:', user)}
/>
svelte

型ガードと型の絞り込み

TypeScriptの型ガードを使用することで、条件分岐内で型を絞り込み、より安全なコードを書くことができます。Svelteのテンプレート内でも、この機能は有効に働きます。

<script lang="ts">
  type Status = 'loading' | 'success' | 'error';
  
  interface LoadingState {
    status: 'loading';
  }
  
  interface SuccessState<T> {
    status: 'success';
    data: T;
  }
  
  interface ErrorState {
    status: 'error';
    error: Error;
  }
  
  type State<T> = LoadingState | SuccessState<T> | ErrorState;
  
  let state: State<string[]> = { status: 'loading' };
  
  // 型ガード関数
  function isSuccess<T>(state: State<T>): state is SuccessState<T> {
    return state.status === 'success';
  }
  
  function isError<T>(state: State<T>): state is ErrorState {
    return state.status === 'error';
  }
</script>

{#if state.status === 'loading'}
  <p>読み込み中...</p>
{:else if isSuccess(state)}
  <!-- TypeScriptはここでstateがSuccessStateであることを認識 -->
  <ul>
    {#each state.data as item}
      <li>{item}</li>
    {/each}
  </ul>
{:else if isError(state)}
  <!-- TypeScriptはここでstateがErrorStateであることを認識 -->
  <p>エラー: {state.error.message}</p>
{/if}
svelte

Snippets(Svelte 5推奨)の型定義

Svelte 5では、従来の<slot>の代わりにSnippetsを使用することが推奨されています。Snippetsはより型安全で柔軟性があり、TypeScriptとの統合も優れています。

Svelte 5の新機能

SnippetsはSvelte 5で導入された新しい機能で、スロットの代替として設計されました。型安全性が向上し、パラメーターの受け渡しがより明確になります。

基本的なSnippetの使用

<!-- Card.svelte -->
<script lang="ts">
  import type { Snippet } from 'svelte';
  
  interface Props {
    title: string;
    header?: Snippet<[{ title: string }]>;
    children: Snippet;  // デフォルトスロットの代わり
    footer?: Snippet<[{ timestamp: Date }]>;
  }
  
  let { title, header, children, footer }: Props = $props();
</script>

<div class="card">
  {#if header}
    {@render header({ title })}
  {/if}
  
  {@render children()}
  
  {#if footer}
    {@render footer({ timestamp: new Date() })}
  {/if}
</div>
svelte

Snippetを使用する親コンポーネント

親コンポーネントでSnippetを定義する際、{#snippet}ブロックを使用します。パラメーターの型は自動的に推論されます。

<!-- 親コンポーネント -->
<script lang="ts">
  import Card from './Card.svelte';
</script>

<Card title="サンプルカード">
  {#snippet header({ title })}
    <h2>{title}</h2>
  {/snippet}
  
  <!-- childrenはデフォルトコンテンツ -->
  <p>カードの本文内容</p>
  
  {#snippet footer({ timestamp })}
    <small>更新日時: {timestamp.toLocaleString()}</small>
  {/snippet}
</Card>
svelte

従来のスロット(後方互換性)

Svelte 5でのスロット

Svelte 5でも<slot>は引き続きサポートされていますが、新しいプロジェクトではSnippetsの使用が推奨されます。既存のコードベースとの互換性のために残されています。

<!-- 従来のスロット方式(非推奨だが動作する) -->
<script lang="ts">
  export let title: string;
</script>

<div class="card">
  <slot name="header" {title} />
  <slot />
  <slot name="footer" timestamp={new Date()} />
</div>
svelte

コンテキストAPI の型定義

SvelteのコンテキストAPIを使用する際も、TypeScriptの型システムを活用することで、コンポーネント間で共有されるデータの型安全性を確保できます。

<!-- Parent.svelte -->
<script lang="ts">
  import { setContext } from 'svelte';
  
  interface AppContext {
    user: {
      id: number;
      name: string;
    };
    theme: 'light' | 'dark';
    updateTheme: (theme: 'light' | 'dark') => void;
  }
  
  const context: AppContext = {
    user: { id: 1, name: 'Alice' },
    theme: 'light',
    updateTheme: (theme) => {
      context.theme = theme;
    }
  };
  
  // キーは Symbol を使用することを推奨
  const CONTEXT_KEY = Symbol('app');
  setContext(CONTEXT_KEY, context);
</script>
svelte

子コンポーネントでのコンテキスト取得

子コンポーネントでコンテキストを取得する際、ジェネリクスを使って型を指定します。

<!-- Child.svelte -->
<script lang="ts">
  import { getContext } from 'svelte';
  
  interface AppContext {
    user: {
      id: number;
      name: string;
    };
    theme: 'light' | 'dark';
    updateTheme: (theme: 'light' | 'dark') => void;
  }
  
  const CONTEXT_KEY = Symbol('app');
  const context = getContext<AppContext>(CONTEXT_KEY);
  
  // contextは型安全に使用できる
  console.log(context.user.name);
  context.updateTheme('dark');
</script>
svelte

ストアの型定義

SvelteのストアシステムもTypeScriptと完全に統合されています。WritableReadableDerivedなどの型を使用して、ストアの型を明確に定義できます。

ストアとRunes

Svelte 5では、新しいRunesシステム($state$derivedなど)が推奨されていますが、従来のストアシステムも引き続きサポートされています。

<script lang="ts">
  import { writable, derived, type Writable, type Readable } from 'svelte/store';
  
  interface User {
    id: number;
    name: string;
    email: string;
  }
  
  // Writableストア
  const user: Writable<User | null> = writable(null);
  
  // Readableストア(derived)
  const userName: Readable<string> = derived(
    user,
    ($user) => $user?.name ?? 'Guest'
  );
  
  // カスタムストア
  function createCounter(initial = 0) {
    const { subscribe, set, update } = writable(initial);
    
    return {
      subscribe,
      increment: () => update(n => n + 1),
      decrement: () => update(n => n - 1),
      reset: () => set(initial)
    };
  }
  
  const counter = createCounter(10);
</script>

<!-- ストアの値を使用 -->
<p>User: {$userName}</p>
<button on:click={counter.increment}>
  Count: {$counter}
</button>
svelte

実践例:型安全なフォームコンポーネント

以下は、TypeScriptの型システムを活用した、完全に型安全なフォームコンポーネントの実装例です。実際に入力して動作を確認できます。

TypeScript の型定義

interface FormData {
  name: string;
  email: string;
  age: number;
  role: 'admin' | 'user' | 'guest';
}
typescript

このフォームは完全に型安全です。TypeScriptが各フィールドの型を検証し、IDEでの自動補完やエラー検出が可能です。

ライブデモ

🎯 ライブデモ: 型安全なフォームコンポーネント

<!-- UserForm.svelte -->
<script lang="ts">
  // フォームデータの型
  interface FormData {
    name: string;
    email: string;
    age: number;
    role: 'admin' | 'user' | 'guest';
  }
  
  // バリデーションエラーの型
  type FormErrors = Partial<Record<keyof FormData, string>>;
  
  // フォームの状態
  let formData: FormData = {
    name: '',
    email: '',
    age: 20,
    role: 'user'
  };
  
  let errors: FormErrors = {};
  let submitted = false;
  let submittedData: FormData | null = null;
  
  // バリデーション
  function validate(): boolean {
    errors = {};
    
    if (!formData.name.trim()) {
      errors.name = '名前は必須です';
    } else if (formData.name.length < 2) {
      errors.name = '名前は2文字以上で入力してください';
    }
    
    if (!formData.email.trim()) {
      errors.email = 'メールアドレスは必須です';
    } else if (!formData.email.includes('@')) {
      errors.email = '有効なメールアドレスを入力してください';
    }
    
    if (formData.age < 0 || formData.age > 120) {
      errors.age = '年齢は0〜120の間で入力してください';
    }
    
    return Object.keys(errors).length === 0;
  }
  
  // リアルタイムバリデーション(名前フィールド)
  function validateName(): void {
    if (!formData.name.trim()) {
      errors.name = '名前は必須です';
    } else if (formData.name.length < 2) {
      errors.name = '名前は2文字以上で入力してください';
    } else {
      delete errors.name;
    }
    errors = errors; // リアクティビティのトリガー
  }
  
  // リアルタイムバリデーション(メールフィールド)
  function validateEmail(): void {
    if (!formData.email.trim()) {
      errors.email = 'メールアドレスは必須です';
    } else if (!formData.email.includes('@')) {
      errors.email = '有効なメールアドレスを入力してください';
    } else {
      delete errors.email;
    }
    errors = errors; // リアクティビティのトリガー
  }
  
  // 送信処理
  function handleSubmit(event: SubmitEvent): void {
    event.preventDefault();
    
    if (validate()) {
      submitted = true;
      submittedData = { ...formData };
      
      // 3秒後に成功メッセージを非表示にする
      setTimeout(() => {
        submitted = false;
      }, 3000);
    }
  }
  
  // リセット処理
  function handleReset(): void {
    formData = {
      name: '',
      email: '',
      age: 20,
      role: 'user'
    };
    errors = {};
    submitted = false;
    submittedData = null;
  }
</script>

<div class="demo-container">
  <h3>🎯 ライブデモ: 型安全なフォームコンポーネント</h3>
  
  <form on:submit={handleSubmit} novalidate>
    <div class="form-group">
      <label for="name">
        名前 <span class="required">*</span>
      </label>
      <input
        id="name"
        type="text"
        bind:value={formData.name}
        on:blur={validateName}
        class:error={errors.name}
        placeholder="山田太郎"
      />
      {#if errors.name}
        <p class="error-message">{errors.name}</p>
      {/if}
    </div>
    
    <div class="form-group">
      <label for="email">
        メールアドレス <span class="required">*</span>
      </label>
      <input
        id="email"
        type="email"
        bind:value={formData.email}
        on:blur={validateEmail}
        class:error={errors.email}
        placeholder="example@example.com"
      />
      {#if errors.email}
        <p class="error-message">{errors.email}</p>
      {/if}
    </div>
    
    <div class="form-group">
      <label for="age">
        年齢
      </label>
      <input
        id="age"
        type="number"
        bind:value={formData.age}
        class:error={errors.age}
        min="0"
        max="120"
      />
      {#if errors.age}
        <p class="error-message">{errors.age}</p>
      {/if}
    </div>
    
    <div class="form-group">
      <label for="role">
        役割
      </label>
      <select id="role" bind:value={formData.role}>
        <option value="admin">管理者</option>
        <option value="user">ユーザー</option>
        <option value="guest">ゲスト</option>
      </select>
    </div>
    
    <div class="button-group">
      <button type="submit" class="btn-primary">
        送信
      </button>
      <button type="button" class="btn-secondary" on:click={handleReset}>
        リセット
      </button>
    </div>
  </form>
  
  {#if submitted && submittedData}
    <div class="success-message">
      <h4>✅ フォームが正常に送信されました!</h4>
      <div class="submitted-data">
        <p><strong>送信されたデータ:</strong></p>
        <pre>{JSON.stringify(submittedData, null, 2)}</pre>
      </div>
    </div>
  {/if}
</div>

<style>
  .demo-container {
    padding: 1.5rem;
    background: var(--vp-c-bg-soft);
    border-radius: 8px;
    margin: 2rem 0;
  }
  
  h3 {
    margin-top: 0;
    margin-bottom: 1.5rem;
    color: var(--vp-c-text-1);
  }
  
  h4 {
    margin-top: 1.5rem;
    margin-bottom: 0.5rem;
    color: var(--vp-c-text-1);
  }
  
  .form-group {
    margin-bottom: 1.25rem;
  }
  
  label {
    display: block;
    margin-bottom: 0.5rem;
    font-weight: 500;
    color: var(--vp-c-text-1);
  }
  
  .required {
    color: #dc2626;
  }
  
  input,
  select {
    width: 100%;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--vp-c-divider);
    border-radius: 4px;
    background: var(--vp-c-bg);
    color: var(--vp-c-text-1);
    font-size: 0.95rem;
    transition: border-color 0.2s, box-shadow 0.2s;
  }
  
  input:focus,
  select:focus {
    outline: none;
    border-color: var(--vp-c-brand);
    box-shadow: 0 0 0 3px rgba(255, 62, 0, 0.1);
  }
  
  input.error {
    border-color: #dc2626;
  }
  
  input.error:focus {
    box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1);
  }
  
  .error-message {
    margin-top: 0.25rem;
    color: #dc2626;
    font-size: 0.875rem;
  }
  
  .button-group {
    display: flex;
    gap: 1rem;
    margin-top: 1.5rem;
  }
  
  button {
    padding: 0.5rem 1.5rem;
    border: none;
    border-radius: 4px;
    font-weight: 500;
    cursor: pointer;
    transition: background-color 0.2s, transform 0.1s;
  }
  
  button:active {
    transform: translateY(1px);
  }
  
  .btn-primary {
    background: var(--vp-c-brand);
    color: white;
  }
  
  .btn-primary:hover {
    background: var(--vp-c-brand-dark);
  }
  
  .btn-secondary {
    background: var(--vp-c-gray-soft);
    color: var(--vp-c-text-1);
  }
  
  .btn-secondary:hover {
    background: var(--vp-c-gray);
  }
  
  .success-message {
    margin-top: 1.5rem;
    padding: 1rem;
    background: #10b98114;
    border: 1px solid #10b981;
    border-radius: 4px;
    animation: slideIn 0.3s ease-out;
  }
  
  @keyframes slideIn {
    from {
      opacity: 0;
      transform: translateY(-10px);
    }
    to {
      opacity: 1;
      transform: translateY(0);
    }
  }
  
  .submitted-data {
    margin-top: 1rem;
  }
  
  .submitted-data p {
    margin-bottom: 0.5rem;
    color: var(--vp-c-text-1);
  }
  
  pre {
    padding: 0.75rem;
    background: var(--vp-c-bg);
    border-radius: 4px;
    overflow-x: auto;
    font-size: 0.875rem;
    color: var(--vp-c-text-1);
  }
  
  .type-info {
    margin-top: 2rem;
    padding: 1rem;
    background: var(--vp-c-bg);
    border-radius: 4px;
    border: 1px solid var(--vp-c-divider);
  }
  
  .type-info code {
    color: var(--vp-c-text-code);
  }
  
  .note {
    margin-top: 0.75rem;
    font-size: 0.875rem;
    color: var(--vp-c-text-2);
    line-height: 1.6;
  }
  
  /* ダークモード対応 */
  :global(.dark) .demo-container {
    background: rgba(30, 30, 30, 0.5);
  }
  
  :global(.dark) input,
  :global(.dark) select {
    background: #1a1a1a;
  }
  
  :global(.dark) .type-info {
    background: #1a1a1a;
  }
  
  :global(.dark) pre {
    background: #0a0a0a;
  }
</style>
svelte
Click fold/expand code

デモの説明

このライブデモでは、TypeScriptを活用した型安全なフォームコンポーネントを実際に操作できます。以下の機能を試してみてください:

📝 試してみる

  1. 入力フィールドの検証

    • 名前フィールドを空のままフォーカスを外すとエラーが表示されます
    • 名前を1文字だけ入力すると「2文字以上」のエラーが表示されます
    • メールアドレスに@を含まない文字列を入力するとエラーになります
  2. リアルタイムバリデーション

    • 名前とメールアドレスはフォーカスが外れた時点(blurイベント)で検証されます
    • エラーがある場合は赤い枠線とエラーメッセージが表示されます
    • 正しい値を入力するとエラーが自動的にクリアされます
  3. フォーム送信

    • すべての必須フィールドが正しく入力されていないと送信できません
    • 送信成功時には、入力されたデータがJSON形式で表示されます
    • 成功メッセージは3秒後に自動的に消えます
  4. リセット機能

    • リセットボタンを押すと、すべてのフィールドが初期値に戻ります
    • エラーメッセージもすべてクリアされます

実装のポイント

このフォームコンポーネントは以下のTypeScript機能を活用しています:

  1. インターフェースによる型定義

    • FormDataインターフェースでフォームデータの構造を定義
    • リテラル型('admin' | 'user' | 'guest')による厳密な型制約
  2. 型エイリアスの活用

    • FormErrors型でバリデーションエラーの構造を定義
    • PartialRecordを組み合わせた柔軟な型定義
  3. イベントハンドラの型安全性

    • SubmitEvent型による送信イベントの型定義
    • MouseEventInputEventなどDOM標準の型を活用
  4. リアルタイムバリデーション

    • 各フィールドのblurイベントで個別バリデーション
    • 型安全性を保ちながら動的なエラー表示

TypeScriptによる開発時のメリット

  • コンパイル時エラー検出: 型の不一致やプロパティの誤りを事前に発見
  • IDE支援: 自動補完、リファクタリング、定義へのジャンプが可能
  • ドキュメント化: 型定義自体がAPIドキュメントとして機能
  • 保守性向上: 大規模なアプリケーションでも安全に変更が可能

ソースコード

完全なソースコードを見る
<!-- UserForm.svelte -->
<script lang="ts">
  // フォームデータの型
  interface FormData {
    name: string;
    email: string;
    age: number;
    role: 'admin' | 'user' | 'guest';
  }
  
  // バリデーションエラーの型
  type FormErrors = Partial<Record<keyof FormData, string>>;
  
  // フォームの状態
  let formData: FormData = {
    name: '',
    email: '',
    age: 20,
    role: 'user'
  };
  
  let errors: FormErrors = {};
  let submitted = false;
  let submittedData: FormData | null = null;
  
  // バリデーション
  function validate(): boolean {
    errors = {};
    
    if (!formData.name.trim()) {
      errors.name = '名前は必須です';
    } else if (formData.name.length < 2) {
      errors.name = '名前は2文字以上で入力してください';
    }
    
    if (!formData.email.trim()) {
      errors.email = 'メールアドレスは必須です';
    } else if (!formData.email.includes('@')) {
      errors.email = '有効なメールアドレスを入力してください';
    }
    
    if (formData.age < 0 || formData.age > 120) {
      errors.age = '年齢は0〜120の間で入力してください';
    }
    
    return Object.keys(errors).length === 0;
  }
  
  // リアルタイムバリデーション(名前フィールド)
  function validateName(): void {
    if (!formData.name.trim()) {
      errors.name = '名前は必須です';
    } else if (formData.name.length < 2) {
      errors.name = '名前は2文字以上で入力してください';
    } else {
      delete errors.name;
    }
    errors = errors; // リアクティビティのトリガー
  }
  
  // 送信処理
  function handleSubmit(event: SubmitEvent): void {
    event.preventDefault();
    
    if (validate()) {
      submitted = true;
      submittedData = { ...formData };
      
      // 3秒後に成功メッセージを非表示
      setTimeout(() => {
        submitted = false;
      }, 3000);
    }
  }
</script>
svelte

TypeScriptの利点

このフォーム実装により、以下のメリットが得られます:

  • コンパイル時の型チェック - 型の不一致を事前に検出
  • IDEの自動補完 - プロパティ名やメソッド名を自動提案
  • リファクタリングの安全性 - 型定義の変更が自動的に波及
  • ドキュメントとしての型 - 型定義がAPIドキュメントの役割を果たす

まとめ

このページで学んだこと

  • SvelteコンポーネントでのTypeScript有効化
  • Propsの型定義とインターフェース
  • DOM/カスタムイベントの型安全な処理
  • ジェネリック型を使った汎用コンポーネント
  • 型ガードによる型の絞り込み
  • コンテキストAPIとストアの型定義
  • 実践的な型安全フォームの実装

次のステップ

TypeScriptとの統合を理解したら、 use:アクション でDOM要素を直接操作する方法を学びましょう。その後、 Runesシステム でSvelte 5の新しいリアクティビティシステムを学びましょう。

Last update at: 2025/08/14 07:39:43