Why SvelteKit Generates Types for You
Throughout this series you have seen import type { PageLoad } from './$types', import type { PageProps } from './$types'. Each route file imports its types from a path that looks like a local relative import but these are a generated in SvelteKit’s hidden .svelte-kit/types/ directory when application is running.
SvelteKit’s automatic type generation is designed to keep your route types in sync with your directory structure and load function outputs. Instead of manually defining types for route parameters and load function return values, SvelteKit creates a $types file for each route in the hidden .svelte-kit/types/ directory. This ensures that the params object, the shape of the data returned from load functions, and the data prop received by components are always accurately typed.
For example, the route src/routes/shop/[category]/[productId] will have a params type of { category: string; productId: string }, and its data type will depend on what the load function returns, such as { product, variants }. In contrast, the route src/routes/blog/[slug] will have a params type of { slug: string }, and its load function might return { post, relatedArticles }, resulting in a different data type. The generated types always reflect the exact structure of your route parameters and load function outputs.
The generated $types file is the contract between your load function and your component. It ensures that if your load function returns { post } and your component references data.psot with a typo, TypeScript catches it at compile time rather than at runtime. This end-to-end type safety, from URL parameter to database result to rendered template, is one of SvelteKit’s genuine strengths, but it only works if you use the right types consistently.
This article is a reference for which types exist, which file each one belongs in, how merged layout data affects the component type, how App.Locals fits in, and the most common type inference problems and how to fix them.
The Type Generation Model
SvelteKit regenerates the .svelte-kit/types/ directory every time you save a file and the dev server is running. It walks your src/routes/ directory, reads the dynamic segment names from directory names, reads the exported symbols from route files, and produces a set of .d.ts files that mirror the route structure.
The ./$types import in a route file is resolved by TypeScript to the corresponding generated file for that route. You never need to look at the generated files directly; they are implementation detail. What matters is knowing the names of the types they export and what each one describes.
Generate types manuallyTo generate the types without server running, use the commandpnpm exec svelte-kit syncin terminal.
A good mental model is this: for every route, SvelteKit generates a type for the load function and a type for the component that receives its output. For a route at src/routes/blog/[slug]/, the generated types include:
| Type | Used in | Purpose |
|---|---|---|
PageLoad | +page.ts | Type of the load function |
PageServerLoad | +page.server.ts | Type of the load function (server-only) |
LayoutLoad | +layout.ts | Type of the load function |
LayoutServerLoad | +layout.server.ts | Type of the load function (server-only) |
PageProps | +page.svelte | Props shape for $props() — includes data and form |
LayoutProps | +layout.svelte | Props shape for $props() — includes data and children |
PageData | anywhere | Merged data shape the page receives |
LayoutData | anywhere | Merged data shape the layout receives |
RouteParams | anywhere | Shape of the params object for this route |
Not every file needs all of these. The rule is: use the type that matches the file you are writing and the role it plays. The sections below explain each one in context.
Load Function Types
PageLoad and PageServerLoad
PageLoad types and PageServerLoad types differ in what the event argument contains, because the two file types have access to different inputs.
PageLoad
PageLoad is used in +page.ts files whose load function runs in both server and client contexts. It has access to params, url, fetch, depends, route, parent, and setHeaders, but not cookies, locals, or any other server-only properties.
It means that if you try to access cookies in a +page.ts file, TypeScript will throw an error because it knows that property does not exist in that context. More importantly, it would be a runtime error because +page.ts code runs in the browser where there is no cookies object on the event.
// src/routes/blog/[slug]/+page.ts
import type { PageLoad } from './$types'
export const load: PageLoad = async ({ params, url, fetch }) => {
// params is typed as { slug: string } from the route structure
const res = await fetch(`/api/posts/${params.slug}`)
const post = await res.json()
return { post }
} PageServerLoad
PageServerLoad is used in +page.server.ts files whose load function runs on the server only. It has access to everything PageLoad has — params, url, fetch, depends, route, parent, and setHeaders — plus cookies, locals, and platform for server-only context such as authentication, sessions, and deployment-specific bindings.
// src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from './$types'
import { error } from '@sveltejs/kit'
export const load: PageServerLoad = async ({ params, cookies, locals, fetch }) => {
// cookies and locals are available here, not in PageLoad
if (!locals.user) error(401, 'Authentication required')
const res = await fetch(`/api/posts/${params.slug}`)
if (res.status === 404) error(404, 'Post not found')
const post = await res.json()
return { post, user: locals.user }
} One important constraint: because +page.server.ts runs only on the server and its return value is serialised and sent to the client, you can only return serialisable data — plain objects, arrays, strings, numbers, and dates. Returning class instances or functions will fail at runtime; TypeScript does not enforce this for you.
The platform property is typed by the App.Platform interface in app.d.ts, which platform adapters extend to expose deployment-specific APIs. With @sveltejs/adapter-cloudflare, for example, platform gives you access to Cloudflare Workers bindings:
// src/app.d.ts (declared by the Cloudflare adapter)
declare global {
namespace App {
interface Platform {
env: {
MY_KV_NAMESPACE: KVNamespace
MY_D1_DATABASE: D1Database
}
context: ExecutionContext
caches: CacheStorage
}
}
} Then in your server load function:
export const load: PageServerLoad = async ({ platform }) => {
// platform.env is typed as Cloudflare bindings
const value = await platform?.env.MY_KV_NAMESPACE.get('some-key')
return { value }
} With adapter-node or adapter-static, App.Platform remains empty ({}), so platform is undefined at runtime. The type narrows accordingly to whatever the installed adapter declares.
LayoutLoad and LayoutServerLoad
LayoutLoad and LayoutServerLoad are the equivalent types for +layout.ts and +layout.server.ts respectively. They have the same capability distinction as the page variants.
The one property unique to layout load types is the children event property, though this refers to the data from child load functions, not to the Svelte children snippet. In practice, most layout loads do not need it directly.
// src/routes/blog/+layout.server.ts
import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = async ({ locals, fetch }) => {
const res = await fetch('/api/posts/index')
const posts = await res.json()
return {
posts,
currentUser: locals.user
}
} Using LayoutLoad or PageServerLoad in a +layout.server.ts file are both mistakes that will produce TypeScript errors when you try to use server-only properties, so the generated types help you catch that at compile time.
Component Data Types
PageData and PageProps
PageData is the type of the merged data object that +page.svelte receives. It combines the return type of the page’s own load function with the data types returned by all ancestor layout load functions. This is a significant point: PageData at a deeply nested route includes data from the root layout load, every intermediate layout load, and the page load itself.
PageProps, introduced in SvelteKit 2.16.0, is a step up from PageData: it wraps PageData in the actual props object the component receives, and adds any other props alongside data. Think of PageData as the shape of the data object — PageProps is the shape of the full $props() destructure.
For a page that has form actions, PageProps looks like this under the hood:
type PageProps = {
data: PageData
form: ActionData // only present when the route has form actions
} Always use PageProps for $props() in +page.svelte. It correctly types both data and form in one import:
<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
import type { PageProps } from './$types'
let { data }: PageProps = $props()
// data.post comes from the page's own load function
// data.posts comes from the blog section layout load (if it exists)
// data.currentUser might come from the root layout load
</script>
<article>
<h1>{data.post.title}</h1>
</article> If the route has form actions, PageProps gives you form for free:
<!-- src/routes/contact/+page.svelte — route with a form action -->
<script lang="ts">
import type { PageProps } from './$types'
let { data, form }: PageProps = $props()
// form is typed as ActionData — the result of the last form submission
</script> With the old PageData approach you would have had to type form manually. PageProps eliminates that.
LayoutData and LayoutProps
LayoutData types the data available to a +layout.svelte. It includes the layout’s own load function return value merged with all ancestor layout data. It does not include data from the child page, because layouts do not receive page data through data; they access it through page.data from $app/state if they need it.
LayoutProps extends LayoutData with the children: Snippet type:
type LayoutProps = {
data: LayoutData
children: Snippet // always present — used to render nested routes
} Every +layout.svelte should use LayoutProps because the children snippet is the mechanism for rendering nested content:
<!-- src/routes/blog/+layout.svelte -->
<script lang="ts">
import type { LayoutProps } from './$types'
let { data, children }: LayoutProps = $props()
// children is the Snippet that renders the matched child route
</script>
<aside>
{#each data.posts as post (post.slug)}
<a href="/blog/{post.slug}">{post.title}</a>
{/each}
</aside>
<main>
{@render children()}
</main> Forgetting children in a layout is a runtime blank screen with no error. Using LayoutProps makes it a compile-time omission rather than a mystery.
When to still use PageData and LayoutData
PageData and LayoutData are still the right types anywhere you need the data shape directly, outside the $props() call — in helper functions, typed variables, or child component prop types:
<script lang="ts">
import type { PageData } from './$types'
function buildOpenGraphTags(data: PageData): Record<string, string> {
return {
'og:title': data.post.title,
'og:description': data.post.excerpt
}
}
</script> <!-- ArticleHeader.svelte — accepts PageData.post shape, not PageProps -->
<script lang="ts">
import type { PageData } from '../routes/blog/[slug]/$types'
let { post }: { post: PageData['post'] } = $props()
</script> The rule is simple: PageProps / LayoutProps go on the $props() call at the top of the component. PageData / LayoutData go everywhere else.
How Merged Data Types Work
The TypeScript merge of layout and page data types is one of the more subtle aspects of SvelteKit’s type system. Understanding it matters because it is the source of several common type errors.
When a page load function and a layout load function both return data, SvelteKit’s PageData type is an intersection of both return types. If the root layout load returns { user: User | null } and the blog layout load returns { posts: Post[] } and the page load returns { post: Post }, then PageData for that page is effectively { user: User | null, posts: Post[], post: Post }.
This intersection is computed by SvelteKit’s type generator, not by hand. The implication is that if you add a new field to any load function in the chain, PageData for all pages within that layout’s scope updates automatically the next time the dev server regenerates types.
Where it becomes tricky is key collision. If both the root layout load and the page load return a key named title, TypeScript will use the page load’s definition, but the generated type may not reflect what you expect. The safest approach is to avoid key collisions in load function return values entirely, using distinct names or namespaced objects.
// This creates a collision between layout and page
// src/routes/+layout.server.ts
export const load = async () => ({ title: 'My Site' })
// src/routes/blog/[slug]/+page.server.ts
export const load = async ({ params }) => ({ title: 'Post Title', post: {} })
// PageData.title is ambiguous; the page version wins at runtime
// but the type generator may produce a union type // Preferred: use distinct, specific names to avoid collisions
// src/routes/+layout.server.ts
export const load = async () => ({ siteName: 'My Site', user: null })
// src/routes/blog/[slug]/+page.server.ts
export const load = async ({ params }) => ({ pageTitle: 'Post Title', post: {} })
// PageData has both siteName and pageTitle with no ambiguity RouteParams and Typed Params
RouteParams is a type alias generated for the params object of each specific route. SvelteKit derives it directly from the directory name: every [segment] becomes a string property. For src/routes/blog/[slug], the generated type is:
// .svelte-kit/types/src/routes/blog/[slug]/$types.d.ts (generated)
export type RouteParams = {
slug: string
} For a route with multiple segments like src/routes/shop/[category]/[productId]:
export type RouteParams = {
category: string
productId: string
} Rest params and optional params produce slightly different shapes. A rest segment [...path] becomes path: string, while an optional segment [[lang]] becomes lang?: string:
// src/routes/[[lang]]/about — optional segment
export type RouteParams = { lang?: string }
// src/routes/files/[...path] — rest segment
export type RouteParams = { path: string } // value is the full path string, e.g. "a/b/c" Why you rarely need it directly
PageLoad and PageServerLoad already use RouteParams internally. When you type your load function, params is already narrowed to the correct route shape — you get params.slug: string without importing anything extra:
// params.slug is string — RouteParams is already applied under the hood
export const load: PageLoad = async ({ params }) => {
const res = await fetch(`/api/posts/${params.slug}`)
return { post: await res.json() }
} When you do need it explicitly
The case where you need RouteParams directly is when code outside a load function needs to accept or work with route params — a utility function in src/lib, a server helper, or a component that constructs a URL from params passed as a prop.
Note the import path in all examples below: outside the route directory you must import from the specific route’s $types file using a path relative to the file doing the importing, not from ./$types.
Let see some practical examples of this:
1: Server utility that builds a canonical URL:
// src/lib/server/seo.ts
import type { RouteParams } from '../../routes/blog/[slug]/$types'
export function buildCanonicalUrl(params: RouteParams): string {
return `https://example.com/blog/${params.slug}`
} 2: Database query helper that accepts params as an argument - avoids repeating the inline type { slug: string } across multiple query functions:
// src/lib/server/db.ts
import type { RouteParams } from '../../routes/blog/[slug]/$types'
import { db } from './client'
export async function getPostByParams(params: RouteParams) {
return db.post.findUnique({ where: { slug: params.slug } })
}
export async function getRelatedPosts(params: RouteParams) {
return db.post.findMany({ where: { slug: { not: params.slug } }, take: 3 })
} Both functions stay in sync automatically: if the route gains a second segment (e.g. [slug]/[version]), RouteParams updates and TypeScript flags every call site that only passes { slug }.
3: Svelte component that receives params as a prop — useful when a component renders a link or breadcrumb based on the current route and gets params drilled down from the page:
<!-- src/lib/components/PostBreadcrumb.svelte -->
<script lang="ts">
import type { RouteParams } from '../../routes/blog/[slug]/$types'
let { params }: { params: RouteParams } = $props()
</script>
<nav>
<a href="/blog">Blog</a>
<span>/</span>
<a href="/blog/{params.slug}">{params.slug}</a>
</nav> Without RouteParams, you’d type the prop as { slug: string } by hand. With it, the component is tied to the actual route structure and will break at compile time if the route ever renames the segment.
App.Locals and Load Function Types
The locals property in PageServerLoad and LayoutServerLoad is typed by App.Locals from src/app.d.ts. This connection between the type declaration and the load function types is automatic: SvelteKit’s generated types reference App.Locals directly, so any change to the App.Locals interface is immediately reflected everywhere locals is used.
// src/app.d.ts
declare global {
namespace App {
interface Locals {
user: { id: string; email: string; role: string } | null
}
}
}
export {} // src/routes/account/+page.server.ts
import type { PageServerLoad } from './$types'
import { error } from '@sveltejs/kit'
export const load: PageServerLoad = ({ locals }) => {
// locals.user is typed as { id: string; email: string; role: string } | null
// because of App.Locals above
if (!locals.user) error(401, 'Sign in required')
return { account: locals.user }
} The same applies to App.PageData. If you declare this interface in app.d.ts, it becomes the type of page.data when read from $app/state, giving the root layout type information about what any page might provide:
// src/app.d.ts
declare global {
namespace App {
interface Locals {
user: { id: string; email: string; role: string } | null
}
interface PageData {
// Optional properties any page might provide
title?: string
description?: string
}
}
}
export {} Common Type Inference Breakdowns
Untyped Load Function Return Values
If you skip PageLoad or PageServerLoad and write the load function as an untyped async function, TypeScript still infers types from the return value, but the params object will be typed as Record<string, string> rather than the precise route-specific shape. More importantly, the component’s data type will not be connected to the load function’s return type; they become two independent inferences that can silently diverge.
// Wrong: Weakly typed, params is Record<string, string>
export async function load({ params, fetch }) {
const res = await fetch(`/api/posts/${params.slug}`)
return { post: await res.json() }
} // Correct: Strongly typed, params.slug is string and tracked
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async ({ params, fetch }) => {
const res = await fetch(`/api/posts/${params.slug}`)
return { post: await res.json() }
} Always annotate the load function with the correct type from ./$types.
Using PageServerLoad in a .ts File
PageLoad is for +page.ts. PageServerLoad is for +page.server.ts. The types themselves are similar in shape, but using the wrong one will either produce TypeScript errors when you try to use server-only event properties like cookies or locals in a +page.ts file, or when you try to return non-serialisable values from a +page.server.ts file.
// Wrong: PageServerLoad in a universal load file
// src/routes/blog/[slug]/+page.ts ← note: not .server.ts
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async ({ params, cookies }) => {
// TypeScript error: cookies does not exist on the universal load event
const theme = cookies.get('theme')
return { theme }
} // Correct: PageLoad for universal, PageServerLoad for server
// src/routes/blog/[slug]/+page.ts
import type { PageLoad } from './$types'
export const load: PageLoad = async ({ params, fetch }) => {
const res = await fetch(`/api/posts/${params.slug}`)
return { post: await res.json() }
} The any Return Type from Unvalidated API Responses
A common type weakness in SvelteKit applications is that await res.json() returns any. This means data.post in the component is typed as any, which defeats the point of the type system entirely. TypeScript will not catch typos like data.post.athuor if the type is any.
The fix is to assert the response type with a type cast or, better, to validate the response using a schema library:
// Weak: post is any
export const load: PageServerLoad = async ({ params, fetch }) => {
const res = await fetch(`/api/posts/${params.slug}`)
return { post: await res.json() }
} // Better: post is typed with a cast (no runtime validation)
type Post = {
id: string
title: string
content: string
publishedAt: string
}
export const load: PageServerLoad = async ({ params, fetch }) => {
const res = await fetch(`/api/posts/${params.slug}`)
const post = (await res.json()) as Post
return { post }
} // Best: post is typed and validated at runtime using valibot
import * as v from 'valibot'
import { error } from '@sveltejs/kit'
const PostSchema = v.object({
id: v.string(),
title: v.string(),
content: v.string(),
publishedAt: v.string()
})
export const load: PageServerLoad = async ({ params, fetch }) => {
const res = await fetch(`/api/posts/${params.slug}`)
const raw = await res.json()
const result = v.safeParse(PostSchema, raw)
if (!result.success) error(500, 'Invalid API response shape')
return { post: result.output }
} Runtime validation is not always necessary, particularly for internal APIs that you control and trust. But as Type casting with a hand-written interface is a reasonable middle ground that at least makes type errors visible at the component level.
PageData vs PageProps Confusion
PageData is the type of the data object itself. PageProps is the type of the component’s props, which wraps data inside an object. They are different types used in different places:
<!-- Wrong: Using PageData where PageProps is needed -->
<script lang="ts">
import type { PageData } from './$types'
let { data }: PageData = $props()
// TypeScript error: PageData is the data shape, not the props shape
</script> <!-- Correct: Use PageProps for the $props() call -->
<script lang="ts">
import type { PageProps } from './$types'
let { data }: PageProps = $props()
// data is now correctly typed as PageData
</script> PageProps was introduced in SvelteKit 2.16.0. Before that version, the correct pattern was:
<script lang="ts">
import type { PageData } from './$types'
let { data }: { data: PageData } = $props()
</script> Both patterns work; PageProps is simply the cleaner shorthand.
Performance and Scaling Considerations
The generated $types files contain simple TypeScript interface and type alias declarations. They are not computationally expensive for the TypeScript compiler to process. Even a large application with hundreds of routes will not experience meaningful type-checking slowdowns from route type generation alone.
The main scaling concern is the any type contamination described above. As an application grows, any types in load function return values propagate into component templates and silently remove the type safety that makes refactoring safe. Periodic audits for any in load function return values, particularly around fetch calls, are worthwhile in large codebases.
A less obvious concern is the size of load function return values. Everything a load function returns is serialised into the page’s HTML payload when it is returned from a +page.server.ts. There is no TypeScript enforcement here; a load function can return a 10MB object and TypeScript will not complain. The type system does not enforce payload size, so runtime monitoring and developer discipline are the right tools for that concern.
Conclusion
SvelteKit’s $types system eliminates the boilerplate of manually declaring data shapes for every route. The generated types connect the load function’s return value to the component’s data prop, the route’s directory structure to the params type, and the App.Locals declaration to every server load function. Used consistently, the result is a codebase where type errors in the data layer surface at compile time rather than at runtime.
The rules are straightforward: PageLoad for +page.ts, PageServerLoad for +page.server.ts, LayoutLoad for +layout.ts, LayoutServerLoad for +layout.server.ts. PageProps for component $props() in pages, LayoutProps for layouts. Always import from ./$types. Always type the response shape when using fetch.
Key Takeaways
SvelteKit generates a $types file for every route that exports PageLoad, PageServerLoad, LayoutLoad, LayoutServerLoad, PageData, PageProps, LayoutData, LayoutProps, and RouteParams. Import from ./$types in every route file to get precise, automatically-maintained types.
PageData is the merged data type including all ancestor layout load function return values. Adding a field to any layout load function automatically adds it to PageData for all pages within that layout’s scope.
App.Locals in src/app.d.ts types event.locals everywhere. App.PageData types page.data when read from $app/state. Both are global type augmentations that propagate automatically.
The await res.json() as Type cast stops any contamination from untyped API responses. Schema validation libraries like Zod add runtime safety on top of the TypeScript type assertion.
What’s Next
With the TypeScript foundation fully in place, the series moves into the advanced performance topics. Streaming lets you return slow data as unresolved promises rather than blocking the initial render on it. The next article, Streaming Data with Promises, covers the pattern, the {#await} syntax for handling streamed state in components, platform caveats, and when streaming is and is not the right tool.
Further Reading
- SvelteKit Generated Types Documentation — official reference for all generated type exports from
$types - SvelteKit App.d.ts Reference — the full list of
Appnamespace interfaces includingLocals,PageData,Error, andPlatform - locals and the Hooks Lifecycle — how
App.Localsis populated inhooks.server.tsbefore load functions run - Universal vs Server Load Functions — the foundational article on when each load file type is appropriate, which determines which load type you import