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.