Svelte TypeScript: Strongly Typed Applications
TypeScript has become the standard for building robust JavaScript applications, and Svelte's first-class TypeScript support makes it easier than ever to build strongly typed reactive UIs. In this tutorial, you'll learn what Svelte TypeScript is, why it matters, how to configure it, and how to apply best practices across components, stores, and APIs.
What Is Svelte TypeScript?
Svelte TypeScript refers to the ability to write Svelte components and application logic using TypeScript's static type system. The Svelte compiler understands .ts files natively, and the <script lang="ts"> directive enables TypeScript inside .svelte single-file components. This gives you compile-time type checking, intelligent autocompletion, and safer refactors without sacrificing Svelte's signature simplicity.
Under the hood, Svelte uses svelte-preprocess (or the built-in Vite plugin in newer SvelteKit versions) to transpile TypeScript before the compiler runs. The official svelte-check tool performs full type diagnostics on your components, similar to tsc but aware of Svelte's template syntax.
Why It Matters
- Catch errors early: Type mismatches surface at compile time, not at runtime in production.
- Better DX: Editors like VS Code provide accurate autocompletion for props, stores, and event handlers.
- Self-documenting code: Types serve as living documentation for component contracts.
- Confident refactors: Renaming a prop or changing a store shape immediately highlights every affected file.
- Team scalability: Larger teams benefit from enforced contracts between modules.
Setting Up a Typed Svelte Project
The fastest way to start is with the official SvelteKit scaffolding tool, which includes TypeScript by default:
npm create svelte@latest my-app
# Choose: Skeleton project, TypeScript syntax
cd my-app
npm install
If you're working with a plain Svelte + Vite project, install the required dependencies:
npm install --save-dev typescript svelte-check @tsconfig/svelte
Create a tsconfig.json at the project root:
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"strict": true,
"moduleResolution": "bundler",
"target": "ESNext",
"module": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"types": ["svelte", "vite/client"]
},
"include": ["src/**/*.ts", "src/**/*.svelte"]
}
Add a type-check script to package.json:
{
"scripts": {
"check": "svelte-check --tsconfig ./tsconfig.json"
}
}
Typed Components
To enable TypeScript inside a component, add lang="ts" to the script tag. Props are declared with the $props() rune (Svelte 5) or the export let syntax (Svelte 4). Here's a Svelte 5 example:
<!-- UserCard.svelte -->
<script lang="ts">
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'editor' | 'viewer';
}
interface Props {
user: User;
onSelect?: (user: User) => void;
}
let { user, onSelect }: Props = $props();
</script>
<article>
<h2>{user.name}</h2>
<p>{user.email}</p>
<span class="badge">{user.role}</span>
<button onclick={() => onSelect?.(user)}>Select</button>
</article>
For Svelte 4, the equivalent looks like this:
<script lang="ts">
export let user: User;
export let onSelect: (user: User) => void = () => {};
</script>
Typed Stores
Svelte stores are generic. You can pass a type argument to writable, readable, and derived to lock down the value type:
// stores/session.ts
import { writable, derived } from 'svelte/store';
export interface Session {
token: string | null;
userId: number | null;
}
export const session = writable<Session>({
token: null,
userId: null
});
export const isLoggedIn = derived(session, ($s) => $s.token !== null);
Consuming the store in a component gives you a typed $session auto-subscription:
<script lang="ts">
import { session, isLoggedIn } from './stores/session';
</script>
{#if $isLoggedIn}
<p>Welcome, user #{$session.userId}</p>
{/if}
Typed Event Handlers
DOM event handlers benefit from TypeScript's Event subtypes. Use the correct event type to access specific properties safely:
<script lang="ts">
function handleSubmit(event: SubmitEvent) {
event.preventDefault();
const form = event.target as HTMLFormElement;
const data = new FormData(form);
console.log(data.get('email'));
}
function handleInput(event: KeyboardEvent) {
const target = event.currentTarget as HTMLInputElement;
console.log(target.value);
}
</script>
<form onsubmit={handleSubmit}>
<input type="email" name="email" onkeydown={handleInput} />
<button type="submit">Save</button>
</form>
Typed Component Events (Svelte 4)
In Svelte 4, you can type dispatched events using createEventDispatcher with a generic interface:
<script lang="ts">
import { createEventDispatcher } from 'svelte';
interface Item {
id: number;
label: string;
}
const dispatch = createEventDispatcher<{
select: Item;
delete: number;
}>();
function select(item: Item) {
dispatch('select', item);
}
</script>
In Svelte 5, prefer callback props (as shown in the UserCard example) over event dispatching for stronger typing and simpler APIs.
Typed API Calls
Define response types for your API layer so components receive well-shaped data:
// api/products.ts
export interface Product {
id: number;
name: string;
price: number;
inStock: boolean;
}
export async function fetchProducts(): Promise<Product[]> {
const res = await fetch('/api/products');
if (!res.ok) {
throw new Error(`Failed to load products: ${res.status}`);
}
return res.json() as Promise<Product[]>;
}
<!-- ProductList.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import { fetchProducts, type Product } from './api/products';
let products: Product[] = $state([]);
let loading = $state(true);
onMount(async () => {
products = await fetchProducts();
loading = false;
});
</script>
{#if loading}
<p>Loading…</p>
{:else}
<ul>
{#each products as product (product.id)}
<li>{product.name} — ${product.price.toFixed(2)}</li>
{/each}
</ul>
{/if}
Working with SvelteKit Load Functions
SvelteKit's load functions are fully typed. Use the PageLoad and LayoutLoad types from $types, which are auto-generated based on your routes:
// src/routes/products/+page.ts
import type { PageLoad } from './$types';
import { fetchProducts } from '$lib/api/products';
export const load: PageLoad = async () => {
const products = await fetchProducts();
return { products };
};
<!-- src/routes/products/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
<ul>
{#each data.products as product (product.id)}
<li>{product.name}</li>
{/each}
</ul>
Best Practices
- Enable strict mode: Set
"strict": trueintsconfig.jsonto catch null and implicitanyissues. - Prefer interfaces for props: Define an explicit
Propsinterface so the component contract is obvious. - Use
typeimports: Import types withimport type { ... }to avoid bundling them at runtime. - Type stores explicitly: Always pass a generic to
writableandderivedeven when it can be inferred. - Run
svelte-checkin CI: Add it to your pipeline so type errors fail the build before deployment. - Avoid
any: Useunknownand narrow with type guards when the shape is uncertain. - Co-locate types: Keep shared interfaces in
$lib/typesand reuse them across components and API modules. - Use
satisfies: Thesatisfiesoperator lets you validate an object against a type while preserving narrower inferred types.
Using satisfies for Safer Config
const theme = {
primary: '#3498db',
danger: '#e74c3c'
} satisfies Record<string, string>;
// theme.primary is still inferred as string, but the object
// is guaranteed to match the Record shape.
Conclusion
TypeScript transforms Svelte from a delightful prototyping tool into a production-grade framework suitable for large, long-lived applications. By typing your props, stores, event handlers, and API responses, you gain compile-time safety and a significantly better developer experience without giving up Svelte's concise syntax. Start with strict mode enabled, run svelte-check regularly, and co-locate your types so they evolve alongside your features. With these foundations in place, your Svelte applications will be easier to refactor, simpler to onboard, and far more resilient to runtime errors.