Introduction
One of the most powerful yet underrated features in SvelteKit is its automatic type generation system. Generated types in SvelteKit: what developers should know because they fundamentally change how we write load functions and actions—catching type mismatches before they reach production. We're going to walk through what the ./$types module gives us, why it matters for your codebase, and how to leverage it effectively in real projects.
The ./$types module and PageLoad
When you create a +page.server.ts or +page.ts file in SvelteKit, the framework automatically generates a type definition file called .svelte-kit/generated/. The key export you'll use is PageLoad.
PageLoad is a generic type that describes what your load function receives and returns. Here's what a typical scenario looks like:
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params, fetch }) => {
const response = await fetch(`/api/products/${params.slug}`);
const product = await response.json();
return {
product,
timestamp: Date.now(),
};
};Notice we're importing PageLoad from ./$types. That file doesn't exist in your project—it's generated by SvelteKit at build time and stored in .svelte-kit/generated/. The type definition knows about your route parameters, any parent load data, and what you're returning. This means TypeScript can validate both the inputs and outputs of your load function automatically.
For a route like src/routes/products/[slug]/+page.server.ts, the generated PageLoad type includes:
params.slug— the dynamic route parameterfetch— SvelteKit's enhanced fetch functiondata— any data from parent load functionsroute— the current route objecturl— the URL object
When you return an object, TypeScript enforces that it matches what the page component expects. If you return something that the component doesn't know about, you'll get a compile-time error.
PageData and what your components receive
While PageLoad describes the load function itself, PageData describes what your Svelte component receives. These are two sides of the same coin.
Inside your +page.svelte, you import and use PageData like this:
import type { PageData } from './$types';
export let data: PageData;Now when you reference data.product or data.timestamp, TypeScript knows exactly what properties are available. If you try to access data.nonexistent, the type checker will catch it.
This is especially powerful when working with nested layouts. If you have a +layout.server.ts that loads user information and a +page.server.ts that loads page-specific content, both contribute to the final PageData. The generated types automatically merge this data, so your component has full visibility into what's available.
// src/routes/+layout.server.ts
export const load: LayoutServerLoad = async () => {
return {
user: { id: 1, name: 'Alice' },
};
};
// src/routes/dashboard/+page.server.ts
export const load: PageLoad = async () => {
return {
analytics: { views: 1000 },
};
};
// src/routes/dashboard/+page.svelte
import type { PageData } from './$types';
export let data: PageData;
// Now data has both data.user and data.analytics
Types update automatically as you edit
One of the biggest quality-of-life improvements that generated types bring is live feedback. Unlike manually-maintained interfaces, these types regenerate whenever you save a file.
Here's the workflow: you modify your load function to return a new field, save the file, and your IDE immediately reflects the change in your component. No separate step. No manual sync. The feedback loop is instant.
// Step 1: You add a new field to your load function
export const load: PageLoad = async () => {
return {
product: { /* ... */ },
relatedProducts: [ /* ... */ ], // New field
};
};
// Step 2: You save
// Step 3: In your component, data.relatedProducts is immediately available
// data.nonRelated would show a type errorThis becomes invaluable in larger teams. When a backend engineer adds a new field to an API response and updates the load function, frontend developers using that component immediately see the type change. No documentation to update, no Slack message needed. The code speaks for itself.
During development, this catches refactoring mistakes early. If you rename a field in your load function but forget to update the component, TypeScript tells you before you deploy.
Catching mistakes at compile time
The real power of generated types is compile-time safety. Instead of discovering bugs in production when a user clicks a button, your type checker catches them during development.
Consider a form action that processes user input:
// src/routes/settings/+page.server.ts
import type { Actions } from './$types';
export const actions: Actions = {
updateProfile: async ({ request, locals }) => {
const data = await request.formData();
const email = data.get('email') as string;
await locals.db.users.update({ email });
return {
success: true,
message: 'Profile updated',
};
},
};The generated type for Actions knows:
- Which action names you've defined
- What parameters each action receives (request, locals, cookies, etc.)
- What they should return
In your component, when you call this action via form submission or the enhance directive, TypeScript validates your usage:
// src/routes/settings/+page.svelte
import { enhance } from '$app/forms';
// TypeScript knows 'updateProfile' exists and is valid
<form method="POST" action="?/updateProfile" use:enhance>
<input type="email" name="email" />
<button>Save</button>
</form>If you mistype the action name as ?/updateProfle, your type checker will complain. If an action handler changes its signature and you're calling it incorrectly, you'll know before runtime.
This also applies to what actions return. If your action returns a specific error shape and you're trying to access a property that doesn't exist, TypeScript catches it.
No manual interfaces needed
Before SvelteKit's generated types, developers manually maintained TypeScript interfaces for load return types and action results. This approach had obvious downsides: interfaces drift out of sync, they're verbose to maintain, and teams often skipped them entirely when pressured for speed.
Generated types eliminate this friction. You define your data once in the load function or action, and SvelteKit automatically makes it available everywhere it's used. No duplication. No synchronization overhead.
Compare the old manual approach:
// Old approach: Manual interface
interface Product {
id: number;
title: string;
price: number;
}
interface ProductPageData {
product: Product;
relatedProducts: Product[];
}
export const load = async (): Promise<ProductPageData> => {
// ...
};
With the generated approach:
// New approach: Let SvelteKit infer it
import type { PageLoad } from './$types';
export const load: PageLoad = async () => {
return {
product: { id: 1, title: 'Widget', price: 29.99 },
relatedProducts: [ /* ... */ ],
};
};
// The type is automatically derived and available in components
The generated version is shorter and self-documenting. If you're returning data, the type follows naturally.
Common mistakes
Forgetting to import from ./$types
The most frequent error is not importing from ./$types at all. Without the import, you lose type safety:
// Wrong: no type annotation
export const load = async () => {
return { product: null };
};
// Right: use PageLoad from ./$types
import type { PageLoad } from './$types';
export const load: PageLoad = async () => {
return { product: null };
};Returning data that doesn't match what components expect
If your component expects data.product.title but your load function returns data.item.name, TypeScript will flag this if you're using the right types everywhere. However, if you skip type annotations, you won't catch the mismatch until runtime.
Ignoring nested load data
In layouts with multiple load functions at different levels, it's easy to forget what data is available from parents. The generated types include all parent data, so lean on your IDE's autocomplete to discover what's available.
Assuming ./$types exists on disk
Developers sometimes try to look at the ./$types file directly. It lives in .svelte-kit/generated/, which is a build artifact. Don't commit it, and don't edit it manually. Let SvelteKit regenerate it.
Key takeaways
- Generated types in SvelteKit automatically create
PageLoadandPageDatatypes based on your load functions and actions. - These types are maintained automatically as you edit, eliminating manual interface management.
- Import from
./$typesin your components and handlers to get compile-time validation of all data flows. - Type safety catches errors at build time instead of in production, especially valuable for form actions and nested data.
- The generated types account for data from parent layouts, merging it seamlessly into the final shape available to components.
- Lean on your IDE's autocomplete when working with
PageDatato discover what properties are available from load functions.






Comments
Loading comments…