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 Matters

Type 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 Feedback

This 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?

  1. Make sure your dev server is running (pnpm dev)
  2. Try restarting the dev server
  3. Run pnpm check to 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 ./$types to get type-safe load function parameters and component props
  • PageProps types 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 PageData and PageServerData based on runtime conditions
  • Form actions get typed via ActionData — return values from form actions are automatically typed in the form prop and page.form
  • satisfies operator validates without widening — use satisfies PageLoad or satisfies PageServerLoad to catch errors while preserving inferred return types
  • Run sync command after file changesnpm run dev or svelte-kit sync regenerates types when you add or modify route files
  • Generated types include action helpers — the Actions type from ./$types provides 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