Zero-effort type safety
More convenience and correctness, less boilerplate
By sprinkling type annotations into your SvelteKit apps, you can get full type safety across the network — thedata
in your page has a type that’s inferred from the return values of theload
functions that generated that data, without you having to explicitly declare anything. It’s one of those things that you come to wonder how you ever lived without.
But what if we didn’t even need the annotations? Sinceload
anddata
are part of the framework, can’t the framework type them for us? This is, after all, what computers are for — doing the boring bits so we can focus on the creative stuff.
As of today, yes: it can.
If you’re using VSCode, just upgrade the Svelte extension to the latest version, and you’ll never have to annotate yourload
functions ordata
props again. Extensions for other editors can also use this feature, as long as they support the Language Server Protocol and TypeScript plugins. It even works with the latest version of our CLI diagnostics toolsvelte-check
!
Before we dive in, let’s recap how type safety works in SvelteKit.
Generated types
In SvelteKit, you get the data for a page in aload
function. Youcould type the event by usingServerLoadEvent
from@sveltejs/kit
:
importtype{interfaceServerLoadEvent<ParamsextendsPartial<Record<string,string>>=Partial<Record<string,string>>,ParentDataextendsRecord<string,any>=Record<string,any>,RouteIdextendsstring|null=string|null>
ServerLoadEvent}from'@sveltejs/kit';exportasyncfunctionfunctionload(event:ServerLoadEvent):Promise<{post:string;}>
load(event:ServerLoadEvent<Partial<Record<string,string>>,Record<string,any>,string|null>
event:interfaceServerLoadEvent<ParamsextendsPartial<Record<string,string>>=Partial<Record<string,string>>,ParentDataextendsRecord<string,any>=Record<string,any>,RouteIdextendsstring|null=string|null>
ServerLoadEvent) {return{post:string
post:awaitconstdatabase:{getPost(slug:string|undefined):Promise<string>;}
database.functiongetPost(slug:string|undefined):Promise<string>
getPost(event:ServerLoadEvent<Partial<Record<string,string>>,Record<string,any>,string|null>
event.RequestEvent<Partial<Record<string,string>>,string|null>.params: Partial<Record<string,string>>
The parameters of the current route - e.g. for a route like/blog/[slug]
, a{ slug: string }
object.
params.string|undefined
post)};}
This works, but we can do better. Notice that we accidentally wroteevent.params.post
, even though the parameter is calledslug
(because of the[slug]
in the filename) rather thanpost
. You could typeparams
yourself by adding a generic argument toServerLoadEvent
, but that’s brittle.
This is where our automatic type generation comes in. Every route directory has a hidden$types.d.ts
file with route-specific types:
import type { ServerLoadEvent } from '@sveltejs/kit';importtype{importPageServerLoadEvent
PageServerLoadEvent}from'./$types';exportasyncfunctionfunctionload(event:PageServerLoadEvent):Promise<{post:any;}>
load(event:PageServerLoadEvent
event:importPageServerLoadEvent
PageServerLoadEvent) {return{post: await database.getPost(event.params.post)post:any
post:awaitdatabase.getPost(event:PageServerLoadEvent
event.params.slug)};}
This reveals our typo, as it now errors on theparams.post
property access. Besides narrowing the parameter types, it also narrows the types forawait event.parent()
and thedata
passed from a serverload
function to a universalload
function. Notice that we’re now usingPageServerLoadEvent
, to distinguish it fromLayoutServerLoadEvent
.
After we have loaded our data, we want to display it in our+page.svelte
. The same type generation mechanism ensures that the type ofdata
is correct:
<scriptlang="ts">importtype{ PageData }from'./$types';exportletdata:PageData;</script><h1>{data.post.title}</h1><div>{@htmldata.post.content}</div>
Virtual files
When running the dev server or the build, types are auto-generated. Thanks to the file-system based routing, SvelteKit is able to infer things like the correct parameters or parent data by traversing the route tree. The result is outputted into one$types.d.ts
file for each route, which looks roughly like this:
importtype*asmodule"@sveltejs/kit"
Kitfrom'@sveltejs/kit';// types inferred from the routing treetypetypeRouteParams={slug:string;}
RouteParams={slug:string
slug:string};typetypeRouteId="/blog/[slug]"
RouteId='/blog/[slug]';typetypePageParentData={}
PageParentData={};// PageServerLoad type extends the generic Load type and fills its generics with the info we haveexporttypetypePageServerLoad=(event:Kit.ServerLoadEvent<RouteParams,PageParentData,string|null>)=>MaybePromise<"/blog/[slug]">
PageServerLoad=module"@sveltejs/kit"
Kit.typeServerLoad<ParamsextendsPartial<Record<string,string>>=Partial<Record<string,string>>,ParentDataextendsRecord<string,any>=Record<string,any>,OutputDataextendsRecord<string,any>|void=void|Record<...>,RouteIdextendsstring|null=string|null>=(event:Kit.ServerLoadEvent<Params,ParentData,RouteId>)=>MaybePromise<OutputData>
The generic form ofPageServerLoad
andLayoutServerLoad
. You should import those from./$types
(seegenerated types)rather than usingServerLoad
directly.
ServerLoad<typeRouteParams={slug:string;}
RouteParams,typePageParentData={}
PageParentData,typeRouteId="/blog/[slug]"
RouteId>;// The input parameter type of the load functionexporttypetypePageServerLoadEvent=Kit.ServerLoadEvent<RouteParams,PageParentData,string|null>
PageServerLoadEvent=typeParameters<Textends(...args:any)=>any>=Textends(...args:inferP)=>any?P:never
Obtain the parameters of a function type in a tuple
Parameters<typePageServerLoad=(event:Kit.ServerLoadEvent<RouteParams,PageParentData,string|null>)=>MaybePromise<"/blog/[slug]">
PageServerLoad>[0];// The return type of the load functionexporttypetypePageData=Kit.ReturnType<any>
PageData=module"@sveltejs/kit"
Kit.typeKit.ReturnType=/*unresolved*/any
ReturnType<typeofimport('../src/routes/blog/[slug]/+page.server.js').load>;
We don’t actually write$types.d.ts
into yoursrc
directory — that would be messy, and no-one likes messy code. Instead, we use a TypeScript feature calledrootDirs
, which lets us map ‘virtual’ directories to real ones. By settingrootDirs
to the project root (the default) and additionally to.svelte-kit/types
(the output folder of all the generated types) and then mirroring the route structure inside it we get the desired behavior:
// on disk:.svelte-kit/├ types/│ ├ src/│ │ ├ routes/│ │ │ ├ blog/│ │ │ │ ├ [slug]/│ │ │ │ │ └ $types.d.tssrc/├ routes/│ ├ blog/│ │ ├ [slug]/│ │ │ ├ +page.server.ts│ │ │ └ +page.svelte
// what TypeScript sees:src/├ routes/│ ├ blog/│ │ ├ [slug]/│ │ │ ├ $types.d.ts│ │ │ ├ +page.server.ts│ │ │ └ +page.svelte
Type safety without types
Thanks to the automatic type generation we get advanced type safety. Wouldn’t it be great though if we could just omit writing the types at all? As of today you can do exactly that:
import type { PageServerLoadEvent } from './$types';exportasyncfunctionfunctionload(event:any):Promise<{post:any;}>
load(event:any
event: PageServerLoadEvent) {return{post:any
post:awaitdatabase.getPost(event:any
event.params.post)};}
<scriptlang="ts">importtype{ PageData }from'./$types';exportletdata:PageData;exportletdata;</script>
While this is super convenient, this isn’t just about that. It’s also aboutcorrectness: When copying and pasting code it’s easy to accidentally getPageServerLoadEvent
mixed up withLayoutServerLoadEvent
orPageLoadEvent
, for example — similar types with subtle differences. Svelte’s major insight was that by writing code in a declarative way we can get the machine to do the bulk of the work for us, correctly and efficiently. This is no different — by leveraging strong framework conventions like+page
files, we can make it easier to do the right thing than to do the wrong thing.
This works for all exports from SvelteKit files (+page
,+layout
,+server
,hooks
,params
and so on) and fordata
,form
andsnapshot
properties in+page/layout.svelte
files.
To use this feature with VS Code install the latest version of the Svelte for VS Code extension. For other IDEs, use the latest versions of the Svelte language server and the Svelte TypeScript plugin. Beyond the editor, our command line toolsvelte-check
also knows how to add these annotations since version 3.1.1.
How does it work?
Getting this to work required changes to both the language server (which powers the IntelliSense in Svelte files) and the TypeScript plugin (which makes TypeScript understand Svelte files from within.ts/js
files). In both we auto-insert the correct types at the correct positions and tell TypeScript to use our virtual augmented file instead of the original untyped file. That in combination with mapping the generated and original positions back and forth gives the desired result. Sincesvelte-check
reuses parts of the language server under the hood, it gets that feature for free without further adjustments.
We’d like to thank the Next.js team forinspiring this feature.
What’s next
For the future we want to look into making even more areas of SvelteKit type-safe — links for example, be it in your HTML or through programmatically callinggoto
.
TypeScript is eating the JavaScript world — and we’re here for it! We care deeply about first class type safety in SvelteKit, and we provide you the tools to make the experience as smooth as possible — one that also scales beautifully to larger Svelte code bases — regardless of whether you use TypeScript or typed JavaScript through JSDoc.