Introduction
By day 62 of building with Svelte and SvelteKit, you've likely noticed that untyped components become harder to maintain as your codebase grows. TypeScript in Svelte is a practical Svelte & SvelteKit guide that bridges the gap between rapid component development and production-grade type safety. We'll walk through the patterns that senior teams use to catch bugs at compile time rather than in production, covering everything from script block configuration to CI validation.
Enabling TypeScript with lang="ts"
The simplest way to start typing your Svelte components is to declare the language in your script block. Adding lang="ts" tells Svelte's compiler to treat your component logic as TypeScript, unlocking type checking and IDE intellisense.
<script lang="ts">
let count: number = 0;
function increment(): void {
count++;
}
</script>
<button on:click={increment}>
Count: {count}
</button>This minimal change gives you immediate feedback. If you later try to assign a string to count, your editor will flag it before you deploy. The TypeScript compiler understands reactive assignments, stores, and event handlers, so you get coverage across your entire component.
For SvelteKit projects, this is already wired up if you scaffolded with TypeScript. Check your svelte.config.js to confirm the preprocessor is enabled. Older projects may need a manual update to include the TypeScript preprocessor in the vite plugin chain.
Typed $props in Svelte 5
Svelte 5 introduced the $props rune, which replaces the older export-based prop pattern and integrates seamlessly with TypeScript. Instead of exporting variables, you destructure props in a single, type-safe statement.
<script lang="ts">
interface UserCardProps {
name: string;
email: string;
role?: 'admin' | 'user' | 'guest';
onUpdate: (email: string) => Promise<void>;
}
const { name, email, role = 'user', onUpdate } = $props<UserCardProps>();
</script>
<div class="card">
<h3>{name}</h3>
<p>Email: {email}</p>
<p>Role: {role}</p>
</div>This pattern is far cleaner than the older approach where you'd declare multiple export statements. TypeScript now knows the exact shape of incoming data, including optional fields and their defaults. When a parent component passes props, your IDE autocompletes available options and catches typos immediately.
For child-to-parent communication, define callback signatures in your props interface. The onUpdate example above shows how to type an async handler—the compiler ensures callers pass a function with that exact signature. This prevents subtle runtime errors where a parent forgets to await or passes the wrong callback entirely.
Generated Types for Load Data
SvelteKit's load functions return data to your components, and TypeScript can auto-generate types for that data. This is where SvelteKit shines: the framework generates type definitions based on your loader's return type, and your component automatically knows what properties are available.
// src/routes/user/[id]/+page.server.ts
import type { PageServerLoad } from './$types';
export const load = (async ({ params }) => {
const userId = params.id;
const user = await fetch(`/api/users/${userId}`).then(r => r.json());
return {
user: {
id: user.id,
name: user.name,
email: user.email,
createdAt: new Date(user.created_at)
}
};
}) satisfies PageServerLoad;Notice the satisfies PageServerLoad syntax. This validates that your loader conforms to SvelteKit's interface without changing the inferred return type. SvelteKit then generates a ./$types.d.ts file in your route directory.
In your component, import from that types file:
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
<div>
<h1>{data.user.name}</h1>
<p>Email: {data.user.email}</p>
<p>Member since: {data.user.createdAt.toLocaleDateString()}</p>
</div>The compiler now knows that data.user exists and has those exact properties. If a junior team member tries to access data.user.phone and that field wasn't returned by the loader, TypeScript catches it before code review. This pattern scales beautifully across dozens of routes, preventing the class of bugs where data shapes drift between server and client.
Using svelte-check in CI
While local IDE feedback is helpful, we catch far more bugs by running TypeScript checks in continuous integration. The Svelte ecosystem provides Run This ensures that no branch can be merged with type errors. The tool reports issues like unused variables, type mismatches in event handlers, and inconsistencies between parent and child component props. It's fast enough to run on every commit and comprehensive enough to catch the subtle bugs that leak into production when type checking is optional. A frequent oversight is leaving event handlers untyped, which allows invalid logic to slip through: Svelte's event handlers receive a native DOM event. Without typing the parameter, you lose autocomplete and type guards. The When facing a complex type, the temptation to write In larger applications, parent components sometimes pass props that don't match child expectations. Strong typing prevents this:svelte-check
// package.json
{
"scripts": {
"check": "svelte-check --tsconfig ./tsconfig.json",
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"svelte-check": "^3.7.0"
}
}
npm run check locally or in CI before deployment:// .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run check
- run: npm run build
Common Mistakes
Forgetting to type event handlers
// ❌ Risky
function handleChange(event) {
const value = event.target.value;
}
// ✅ Safe
function handleChange(event: Event): void {
const target = event.target as HTMLInputElement;
const value = target.value;
}
as cast lets TypeScript know you've narrowed the type to a specific element, making properties like value accessible.Using
any to avoid typingconst data: any is strong. This defeats the purpose of TypeScript entirely. Instead, use interfaces or generics to model the real shape:// ❌ Defeats type safety
let data: any = fetch(url).then(r => r.json());
// ✅ Explicit and safe
interface ApiResponse {
status: number;
payload: {
items: Array<{ id: string; name: string }>;
};
}
let data: ApiResponse = await fetch(url)
.then(r => r.json())
.then(d => d as ApiResponse);
Mismatching parent and child prop types
// ❌ Child expects string, parent might pass number
export let userId = '';
// ✅ Explicit type prevents misuse
export let userId: string;
Key Takeaways
lang="ts" in your script blocks to unlock TypeScript checking for component logic and event handlers.$props rune to define typed component interfaces, making parent-child communication explicit and type-safe.$types to sync server data shapes with client components, preventing data-shape drift.svelte-check in CI to catch type errors before deployment, ensuring consistency across your entire team.any types and always specify event handler parameter types to maintain full type coverage.satisfies keyword in loaders to validate your shape without changing inferred types.






Comments
Loading comments…