Type Safety by Default

Svelte 5’s runes work beautifully with TypeScript. Most types are inferred automatically. When you need explicit types, the syntax is clean.

Typing $state

Type inference usually handles $state:

<script lang="ts">
  let count = $state(0);        // number
  let name = $state('Alice');   // string
  let active = $state(true);    // boolean
</script>

For complex types or when starting with null, be explicit:

<script lang="ts">
  interface User {
    id: string;
    name: string;
    email: string;
  }
  
  let user = $state<User | null>(null);
  
  // Later...
  user = { id: '1', name: 'Alice', email: 'alice@example.com' };
</script>

Arrays benefit from explicit types too:

<script lang="ts">
  interface Todo {
    id: number;
    text: string;
    done: boolean;
  }
  
  let todos = $state<Todo[]>([]);
</script>

Typing $props

For simple props, inference works:

<script lang="ts">
  let { name, count = 0 } = $props();
  // name: any (no inference without usage)
  // count: number (inferred from default)
</script>

For proper typing, define an interface:

<script lang="ts">
  interface Props {
    name: string;
    count?: number;
    variant?: 'primary' | 'secondary';
    onclick?: (event: MouseEvent) => void;
  }
  
  let { name, count = 0, variant = 'primary', onclick }: Props = $props();
</script>

Now TypeScript enforces correct usage:

<!-- Parent component -->
<Button name="Save" />                    <!-- ✓ -->
<Button name="Save" count={5} />          <!-- ✓ -->
<Button name="Save" variant="danger" />   <!-- ✗ Error: invalid variant -->
<Button count={5} />                      <!-- ✗ Error: missing name -->

Typing $bindable

Mark bindable props in the interface:

<script lang="ts">
  interface Props {
    value: string;
  }
  
  let { value = $bindable('') }: Props = $props();
</script>

The parent can now use bind::

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

<TextInput bind:value={text} />

Typing Snippets

Snippets use the Snippet type from Svelte:

<script lang="ts">
  import type { Snippet } from 'svelte';
  
  interface Props {
    header?: Snippet;
    children: Snippet;
    footer?: Snippet;
  }
  
  let { header, children, footer }: Props = $props();
</script>

<div class="card">
  {#if header}
    <header>{@render header()}</header>
  {/if}
  
  <main>{@render children()}</main>
  
  {#if footer}
    <footer>{@render footer()}</footer>
  {/if}
</div>

For snippets that receive parameters:

<script lang="ts">
  import type { Snippet } from 'svelte';
  
  interface Item {
    id: string;
    name: string;
  }
  
  interface Props {
    items: Item[];
    renderItem: Snippet<[Item, number]>;  // [item, index]
  }
  
  let { items, renderItem }: Props = $props();
</script>

<ul>
  {#each items as item, index}
    <li>{@render renderItem(item, index)}</li>
  {/each}
</ul>

Usage:

<List items={users}>
  {#snippet renderItem(user, index)}
    <span>{index + 1}. {user.name}</span>
  {/snippet}
</List>

Typing $derived

Derived values are automatically typed based on the expression:

<script lang="ts">
  let items = $state<number[]>([1, 2, 3]);
  
  let total = $derived(items.reduce((a, b) => a + b, 0));  // number
  let doubled = $derived(items.map(n => n * 2));          // number[]
  let first = $derived(items[0]);                          // number | undefined
</script>

For $derived.by, the return type is inferred:

<script lang="ts">
  let items = $state<number[]>([1, 2, 3]);
  
  let stats = $derived.by(() => {
    const sum = items.reduce((a, b) => a + b, 0);
    return {
      sum,
      avg: sum / items.length,
      count: items.length
    };
  });
  // stats: { sum: number, avg: number, count: number }
</script>

Generic Components

Create reusable typed components with generics:

<script lang="ts" generics="T">
  import type { Snippet } from 'svelte';
  
  interface Props {
    items: T[];
    renderItem: Snippet<[T]>;
    keyFn?: (item: T) => string | number;
  }
  
  let { items, renderItem, keyFn = (item) => JSON.stringify(item) }: Props = $props();
</script>

{#each items as item (keyFn(item))}
  {@render renderItem(item)}
{/each}

TypeScript infers T from usage:

<script lang="ts">
  interface User {
    id: string;
    name: string;
  }
  
  let users: User[] = $state([]);
</script>

<!-- T is inferred as User -->
<List items={users} keyFn={(u) => u.id}>
  {#snippet renderItem(user)}
    <span>{user.name}</span>  <!-- user is typed as User -->
  {/snippet}
</List>

Common Patterns

Event handlers

<script lang="ts">
  interface Props {
    onclick?: (event: MouseEvent) => void;
    onsubmit?: (data: FormData) => Promise<void>;
  }
  
  let { onclick, onsubmit }: Props = $props();
</script>

Optional vs required

<script lang="ts">
  interface Props {
    id: string;              // Required
    label?: string;          // Optional
    disabled?: boolean;      // Optional with undefined default
  }
  
  let { id, label, disabled = false }: Props = $props();
</script>

Rest props

<script lang="ts">
  import type { HTMLInputAttributes } from 'svelte/elements';
  
  interface Props extends HTMLInputAttributes {
    label: string;
    error?: string;
  }
  
  let { label, error, ...rest }: Props = $props();
</script>

<label>
  {label}
  <input {...rest} />
  {#if error}<span class="error">{error}</span>{/if}
</label>

TypeScript makes your components self-documenting. The types are the API contract.