Type Safety in SvelteKit
One of SvelteKit’s most powerful features is its automatic type generation. If you’re using TypeScript, SvelteKit ensures that the data you return from your load function is exactly what your component receives — with full IntelliSense and compile-time error checking.
The reason this is valuable in practice: without generated types, changing a load function’s return shape is silent. You rename a field, and the component template still references the old name. Nothing complains at build time. You ship the bug to production and it surfaces as a runtime error when a user hits that page.
With SvelteKit’s generated types, renaming a field in a load function immediately highlights every component that references the old name. The type system becomes a refactoring assistant — you make one change and follow the red underlines until everything is consistent again.
Why Type Safety MattersType safety isn’t just about catching bugs—it’s about developer confidence. When you refactor a load function, TypeScript will instantly show you every component that needs updating. This is the difference between “I hope I didn’t break anything” and “I know exactly what I changed.”
The $types Magic
When you run your dev server (npm run dev), SvelteKit watches your route files. Every time you save a +page.js, +page.server.js, or layout file, it analyses what you’re returning and generates type definitions automatically.
You access these generated types from a special virtual module: ./$types.
// src/routes/blog/+page.ts
import type { PageLoad } from './$types' // ← Generated by SvelteKit
export const load: PageLoad = async ({ params }) => {
return {
title: 'Hello',
count: 42
}
} Why This Catches Real Bugs
Here’s a concrete scenario. You have a working blog page:
// +page.ts
export const load: PageLoad = async () => {
return { posts: await fetchPosts() }
} <!-- +page.svelte -->
<script lang="ts">
let { data }: PageProps = $props()
</script>
<h1>{data.posts.length} posts</h1> Now you refactor and rename the field:
// +page.ts — renamed 'posts' to 'articles'
export const load: PageLoad = async () => {
return { articles: await fetchPosts() }
} Without types, this compiles fine. The component quietly breaks at runtime — data.posts is now undefined, and your users see an error. With SvelteKit’s generated types, your editor immediately shows:
<h1>{data.posts.length} posts</h1>
<!-- ^^^^^ Property 'posts' does not exist on type '{ articles: Post[] }' --> You find the break in seconds, before it ever ships. This is the core value proposition: types make refactoring safe.
How It Works
Step 1: Type Your Load Function
The PageLoad type gives you full autocomplete for all available parameters, including the exact shape of params inferred from your folder names:
// src/routes/blog/[slug]/+page.ts
import type { PageLoad } from './$types'
export const load: PageLoad = async ({ params, fetch, url }) => {
// TypeScript knows:
// - params.slug exists (from the [slug] folder name)
// - fetch is the special SvelteKit fetch
// - url is a URL object
const response = await fetch(`/api/posts/${params.slug}`)
return {
post: await response.json(),
relatedPosts: []
}
} Step 2: Type Your Component
The PageProps type knows exactly what your load function returns:
<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
import type { PageProps } from './$types'
let { data }: PageProps = $props()
// TypeScript knows:
// - data.post exists
// - data.relatedPosts exists
// - data.post is the return type from your API
</script>
<h1>{data.post.title}</h1> The End-to-End Flow
flowchart
A["+page.ts returns { title, posts }"] --> B["SvelteKit generates types"]
B --> C["PageProps knows data.title, data.posts"]
C --> D["Component gets autocomplete"]
style A fill:#fff3e0, color:#000
style B fill:#e1f5fe, color:#000
style C fill:#f3e5f5, color:#000
style D fill:#e8f5e9, color:#000 If you change your load function:
// Before
return { title: 'Hello', posts: [] }
// After - removed 'title'
return { posts: [] } Your component immediately shows an error:
<h1>{data.title}</h1>
<!-- ^^^^^ Property 'title' does not exist --> You catch bugs before you even run the code!
Instant FeedbackThis instant feedback loop is why TypeScript shines in SvelteKit. Change a type in one place, see all affected code immediately highlighted.
Available Types
Page Types
// src/routes/blog/+page.ts
import type { PageLoad } from './$types'
// src/routes/blog/+page.server.ts
import type { PageServerLoad } from './$types'
// src/routes/blog/+page.svelte
import type { PageProps } from './$types' Layout Types
// src/routes/+layout.ts
import type { LayoutLoad } from './$types'
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types'
// src/routes/+layout.svelte
import type { LayoutProps } from './$types' Server Endpoint Types
// src/routes/api/posts/+server.ts
import type { RequestHandler } from './$types'
export const GET: RequestHandler = async ({ params, url }) => {
// ...
} Route Parameters Are Typed
SvelteKit infers parameter types from your folder structure:
src/routes/blog/[slug]/+page.ts
^^^^^^
params.slug: string src/routes/users/[id]/posts/[postId]/+page.ts
^^^^ ^^^^^^^^
params.id: string
params.postId: string // src/routes/users/[id]/posts/[postId]/+page.ts
import type { PageLoad } from './$types'
export const load: PageLoad = async ({ params }) => {
// TypeScript knows both params exist
const userId = params.id // Correct - string
const postId = params.postId // Correct - string
const other = params.other // Error: Property 'other' does not exist
} Parent Data Is Typed
When accessing parent data, TypeScript knows what’s available:
// src/routes/+layout.server.ts
export const load = async () => {
return {
user: { name: 'John', email: 'john@example.com' }
}
} // src/routes/dashboard/+page.ts
import type { PageLoad } from './$types'
export const load: PageLoad = async ({ parent }) => {
const parentData = await parent()
// TypeScript knows parentData.user exists!
console.log(parentData.user.name)
return {
dashboardData: []
}
} Form Action Types
Form actions also get full type safety:
// src/routes/login/+page.server.ts
import type { Actions, PageServerLoad } from './$types'
import { fail } from '@sveltejs/kit'
export const load: PageServerLoad = async () => {
return { message: 'Please log in' }
}
export const actions: Actions = {
default: async ({ request }) => {
const data = await request.formData()
const email = data.get('email')
if (!email) {
return fail(400, {
error: 'Email is required',
email: '' // Return for form repopulation
})
}
return { success: true }
}
} <!-- src/routes/login/+page.svelte -->
<script lang="ts">
import type { PageProps } from './$types'
let { data, form }: PageProps = $props()
// TypeScript knows:
// - data.message: string
// - form?.error: string | undefined
// - form?.success: boolean | undefined
</script>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
{#if form?.success}
<p class="success">Logged in!</p>
{/if} The .svelte-kit Directory
SvelteKit generates types in a hidden .svelte-kit/types directory. You don’t need to look at these files, but understanding they exist helps explain the magic:
.svelte-kit/
└── types/
└── src/
└── routes/
├── $types.d.ts ← Root route types
├── blog/
│ ├── $types.d.ts ← /blog types
│ └── [slug]/
│ └── $types.d.ts ← /blog/[slug] types These are generated automatically and should be in your .gitignore.
TypeScript Configuration
SvelteKit’s tsconfig.json is pre-configured to understand $types:
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"strict": true
}
} The extended config includes path aliases that make ./$types work.
Common Patterns
Typing API Responses
// src/lib/types.ts
export interface Post {
id: string
title: string
content: string
createdAt: string
} // src/routes/blog/+page.ts
import type { PageLoad } from './$types'
import type { Post } from '$lib/types'
export const load: PageLoad = async ({ fetch }) => {
const response = await fetch('/api/posts')
const posts: Post[] = await response.json()
return { posts }
} Shared Types Across Routes
// src/lib/types.ts
export interface User {
id: string
name: string
email: string
}
export interface LayoutData {
user: User | null
} Type Guards for Error Handling
<script lang="ts">
import type { PageProps } from './$types'
let { data }: PageProps = $props()
// Type guard for optional data
function hasPost(d: typeof data): d is typeof data & { post: NonNullable<typeof data.post> } {
return d.post !== null
}
</script>
{#if hasPost(data)}
<h1>{data.post.title}</h1>
{:else}
<p>No post found</p>
{/if} Troubleshooting
Types Not Updating?
- Make sure your dev server is running (
pnpm dev) - Try restarting the dev server
- Run
pnpm checkto regenerate types
Import Errors?
Make sure you’re importing from ./$types, not $types:
// AVOID
import type { PageLoad } from '$types'
// PREFERRED
import type { PageLoad } from './$types' Red Squiggles in New Files?
After creating a new route, save the file and wait a moment for types to generate. Sometimes you need to close and reopen the file in your editor.
Conclusion
SvelteKit’s automatic type generation transforms TypeScript from a nice-to-have into a fundamental development accelerator. By analyzing your route files and generating comprehensive types, it eliminates the entire category of bugs related to prop mismatches, incorrect parameter types, and API contract violations.
The PageData, PageServerData, and LayoutData types flow automatically from load functions to components without manual type definitions or error-prone type assertions.
Mastering type safety in SvelteKit means embracing the ./$types convention, understanding how type narrowing works with discriminated unions, and leveraging generated types for forms and actions. When types flow automatically from server to client, from load functions to components, and from forms to action handlers, you build applications where entire classes of runtime errors become compile-time warnings.
This isn’t just about catching bugs—it’s about building with confidence, refactoring without fear, and letting the type system guide development.
Key Takeaways
- SvelteKit auto-generates types from route files in
.svelte-kit/types— import them via./$typesto get type-safe load function parameters and component props PagePropstypes flow automatically from load function return values to component props:let { data }: PageProps = $props()in Svelte 5 gives you full type safety based on what your load function returns- Discriminated unions enable type narrowing — use type guards to differentiate between
PageDataandPageServerDatabased on runtime conditions - Form actions get typed via
ActionData— return values from form actions are automatically typed in theformprop andpage.form satisfiesoperator validates without widening — usesatisfies PageLoadorsatisfies PageServerLoadto catch errors while preserving inferred return types- Run
synccommand after file changes —npm run devorsvelte-kit syncregenerates types when you add or modify route files - Generated types include action helpers — the
Actionstype from./$typesprovides autocomplete and validation for form action exports - TypeScript catches routing errors at build time — incorrect params, mismatched data shapes, and invalid action responses are caught before deployment
See Also
- Official SvelteKit Documentation - Type Safety
- Generated Types - Understanding
./$typesimports - Form Actions - Typed action handlers
- TypeScript satisfies - Validation without type widening
- Discriminated Unions - Type narrowing patterns