This repository shows how Svelte
and SvelteKit
work together with TypeScript
.
This repository should offer an overview of all
TypeScript
related topics forSvelte
andSvelteKit
Feel free to contribute by creating a PR or open an issue if you think something is missing.
Hi, I'm Ivan, a passionate webdeveloper.
I recently have been working more intensively with TypeScript
when I have created an
internationalization library focusing on developer experience with strong typesafety features:
typesafe-i18n
I know and love Svelte
for a few years now. Over the years I saw how my development workflow
improved and now together with TypeScript
, I'm able to build real business applications with
confidence. When I started with Svelte
, the missing TypeScript
support always bothered me. And
once I could use TypeScript
in my Svelte
projects I still found it not so easy because of some
missing documentation. That's why I decided to create this repository with some examples that should
help you learn the concepts better. I hope you find it useful.
Become a sponsor ❤️ if you want to support my open source contributions.
- Project Setup: good to know before getting started
- Examples: see
TypeScript
+Svelte
in action - TypeScript Tipps: tipps to increase the typesafety of your projects
- Conclusion: some final words
- JSDoc comments: benefit from type-checkings without writing
TypeScript
code
In order to get the best development expeience you should use
VS Code
as your IDE and install the
official Svelte
extension.
The extension offers you a variety of features like code-completions, hover infos, syntax error
highlighting and much more.
I also recommend to install the
Error Lens
extension that displays error-messages inline next to the actual code.
You can create a new SvelteKit
project by following the
Getting started
guide in the official
docs.
npm init svelte my-app
cd my-app
npm install
npm run dev
The npm init svelte my-app
command starts an interactive project-setup process where you get
asked a few questions. Of course you should select the TypeScript
option.
In the root of the generated folder, you should see a
tsconfig.json
file.
I recommend you to configure TypeScript
as strict as possible to benefit from the advanced
type-checking features.
As of writing these are the strictest options I know. This list will grow with new
TypeScript
releases.
Please create a PR if you know more options that should be enabled.
{
"compilerOptions": {
"strict": true,
"allowUnreachableCode": false,
"exactOptionalPropertyTypes": true,
"noImplicitAny": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true
}
}
Not all options may fit your coding style. You can always remove some of them but be aware that this could eliminate some cool type-checking features.
Svelte has added a
TypeScript
-section to their official docs.
Per default the Svelte
-compiler only understands plain HTML
, CSS
and JavaScript
. But we can
add support for other languages to the compiler via custom
preprocessors
. Luckily, we don't have to
write our own preprocessor because there exists already an
official package we can use. svelte-preprocess
enables us to use TypeScript
and also custom CSS syntax like SCSS
or PostCSS
without much
effort. If you take a look at the
svelte.config.js
file,
you see that this was arleady set up for us.
The next step will be to create our first Component and use TypeScript
inside the script
-tag.
<script>
export let name: string
</script>
Hello {name}!
If everything is correctly set up, you should see an error message telling you something like
'Unexpected Token'
. That's because we have to tell the preprocessor that we want to use
TypeScript
syntax. We can do that by adding the lang="ts"
attribute to our script
tag.
-<script>
+<script lang="ts">
export let name: string
</script>
Hello {name}!
That's it. You are now ready to write TypeScript
code inside your Svelte
-components.
You can also import functions from other TypeScript
files like you would in a normal .ts
file.
If you import types from another file, make sure you use the
import type
-syntax
<script>
import { myFunction } from './my-file'
import type { MyFunctionType } from './my-file'
</script>
if you are using a TypeScript
version >= 4.5.x
you can also write it like this:
<script>
import { myFunction, type MyFunctionType } from './my-file'
</script>
You should know that even if you have TypeScript
errors in your code, the Svelte
-compiler will
generate your component (if the code contains valid TypeScript
syntax) and the browser will run
the code normally. That's because the preprocessor
only transpiles TypeScript
to JavaScript
and doesn't perform any type-checking. That's a reason why we should use the Svelte
extension that
will perform the type-checking for the components we have opened in VS Code
. Performing a global
type-check for all components each time you save a file may be too resource intensive for most
computers, so only errors for opened files will show up.
This approach has a downside: If we change something in a component and haven't opened the file where we use that specific component, we won't get notified about errors. Again luckily for us there exists a solution to this problem: a package called svelte-check.
If you take a look at the scripts
section of the package.json
, you will see that it is already
configured for us. We simply can run the following command to perform a check of the whole project:
npm run check
You should include this
svelte-check
script in yourCI/CD
process to get notified if your components containTypeScript
errors.
You may see imports from paths that are not npm modules and also not relative paths e.g.
$components/Component.svelte
. These are called path aliases and you can define them yourself if you like.
Let's say instead of using a relative file import like ../../../components/Component.svelte
you want to
use the alias import $components/Component.svelte
. To do that, you only need to define the desired alias
in your svelte.config.js
file:
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
alias: {
$components: 'src/components',
}
}
};
These aliases are automatically passed to Vite and TypeScript. You can define as many aliases as you want.
The next sections contain a list of all examples included in this repo. It is recommended to explore
the examples inside VS Code
to have proper syntax-highlighting in place.
- the title links directly to the folder, where the example is located
- there is also a short description what is included in the example
- a link to the official documentation to gain more information (if available)
- most examples contain a
Component.svelte
and anUsage.svelte
file to show it in action
I recommend going over the examples in this order since some examples build on top of each other.
https://svelte.dev/docs#component-format-script-1-export-creates-a-component-prop
This chapter teaches you everything about how you can use TypeScript
to improve your component's
props.
- basics: how to define types for props
- optional props: how to mark props as optional
- control flow: how does it work inside the html markup (TS-tipp #1)
- reactive assignments:
how to type reactive statements
https://svelte.dev/docs#component-format-script-3-$-marks-a-statement-as-reactive
- generic props: how to use generics for props
- $$Props: how to extend the defined props of your components
- call exported function: how to call an exported function from the parent
- either or: expect either param A or param B
- svelte:component:
getting prop-types of a component (useful for
svelte:component
) - prop controls type: using a prop to control the type of another prop e.g passing single vs multiple items
In this chapter you will learn how to interact directly with DOM-elements.
- DOM element bindings:
how to type your element bindings
https://svelte.dev/docs#template-syntax-element-directives-bind-this
- actions:
how to define actions in a typesafe way
https://svelte.dev/docs#template-syntax-element-directives-use-action
https://svelte.dev/docs#run-time-svelte-createeventdispatcher
This chapter shows how you can define events that a component emits.
- basics: how to emit events
- typed event details: how to type the emitted events (TS-tipp #2)
- generic events: how to use generics in your events
- $$Events: how to extend the defined events of your components
- strictEvents: how to disallow to listen for other events than the ones defined in a component
- basics: how to define slots and expose props
- named:
how to define named slots and expose props
- $$Slots: how to extend the defined slots of your components
-
SvelteComponent: how to write type definitions for external components
-
svelte-kit package
: how to create aSvelte
component librarySvelteKit
includes an easy way to export single components or component libraries. Just runnpm run package
andSvelteKit
will export all your components from thesrc/lib
directory, together withTypeScript
definitions into thepackage
folder. This folder then also contains a generatedpackage.json
file. After that, you only need to runnpm publish
inside this folder to upload the library tonpm
.
- stores:
how to type your stores
- context
- inline:
how to type contexts
- outsourced:
how to wrap contexts with
TypeScript
(TS-tipp #3)
- inline:
how to type contexts
- type cast in markup: how to cast a type within the markup
In this chapter you get to know how to type the backend of your SvelteKit
application.
-
hooks: how to intercept and modify requests
The
src/hooks.server.ts
file can export four functions. The type of these functions have the same name like the function and get exported from@sveltejs/kit
.import type { HandleFetch, Handle, HandleServerError } from '@sveltejs/kit' export const handle: Handle = async ({ event, resolve }) => { /* implementation */ } export const handleError: HandleServerError = async ({ error, event }) => { /* implementation */ } export const handleFetch: HandleFetch = async (request) => { /* implementation */ }
The
handle
andgetSession
function will have access to thelocals
and thesession
object. To letTypeScript
know how the type of these objects look like, you need to go into thesrc/app.d.ts
file and update the already existinginterfaces
there.Since these types will be shared across multiple files and functions, it makes sense to define them just a single time.
SvelteKit
is configured in a way that it automatically uses those types for all functions.
-
endpoints: how to use
SvelteKit
as an API-endpointWe can use
RequestHandler
to type our endpoints. It expects a single generics:- The type describes the shape the returned value will have.
src/routes/product/[id].ts
import type { RequestHandler } from './$types' import type { Product } from '$models/product.model' import db from '$db' type OutputType = { product: Product } export const GET: RequestHandler<OutputType> = async ({ params }) => { const data = await db.getProjectById(params.id) return { body: { product: data, }, } }
Note: SvelteKit auto-generates the
./$types
folder for us. We can use it to get aRequestHandler
type that already has the correct shape for theparams
object.
-
load function: how to load data before the page gets rendered
Use the
Load
inferface type load functions in your route. It expects two generics:- The first type will be the output type of your
endpoint
if available.
If noGET
-endpoint is defined, theprops
object will beundefined
. - The second type describes the shape the returned value will have.
src/routes/product/[id]/+page.svelte
import type { PageLoad, PageLoadData } from './$types' import type { GET } from './[id]' type OutputProps = PageLoadData & { id: string } // the same as // type OutputProps = { // id: string // product: Product // } export const load: PageLoad<OutputProps> = async ({ params, props }) => { return { props: { id: params.id, product: props.product, }, } }
src/routes/product/[id]/+page.svelte
<script lang="ts"> import type { PageData } from './$types' // PageData = { id: string; product: Product } export let data: PageData </script>
Note: SvelteKit auto-generates the
./$types
folder for us. We can use it to get aLoad
type that already has the correct shape for theparams
object. - The first type will be the output type of your
-
auto generated types
SvelteKit creates some types automatically. Useful when you want to type your Endpoints and Load functions. Those types contain a typedparams
object depending on the route folder structure you use. The types are generated inside the./$types
folder.
The types are generated when you run the dev servernpm run dev
. If you just want to generate the types, without running the dev server you can runnpx svelte-kit sync
. When you runnpm install
, the types will be generated automatically because the SvelteKit runs a post-install script that generates the files.
Here are some examples how you could improve your code base by adding stronger type definitions for common use cases. Not everything needs to be typed that strong. Stricter type definitions will also add complexity you need to maintain, but it certainly improves the devloper experience when using strong typed functions within the code base.
You can use union types to narrow down types in the control flow of your application. Somewhere you probably need to fetch data from an api. The fetch function will probably either return data or an error. It is not wrong to model it like in the following example:
interface ApiResponse<T> {
success: boolean
data: T | undefined
error: Error | undefined
}
But it doesn't work that well when you now want to access the data
object because its type
definition also contains undefined
:
let response: ApiResponse<string>
if (response.success) {
// `response.data` is of type `string | undefined`
} else {
// `response.error` is of type `Error | undefined`
}
We can improve the example by spitting our interface and then using an union type:
// will contain data but no Error
export interface SuccessResponse<T> {
success: true
data: T
error: undefined
}
// will contain an Error but no data
export interface ErrorResponse {
success: false
data: undefined
error: Error
}
// our union type
export type ApiResponse<T> = SuccessResponse<T> | ErrorResponse
If we now access the data
we will see that its type is no longer undefined
:
let response: ApiResponse<string>
if (response.success) {
// `response.data` is of type `string`
} else {
// `response.error` is of type `Error`
}
So whenever you know that something is either A or either B you should also model it that way by splitting the model into two different interfaces and then use an union type to.
You can learn more about union types in the official
TypeScript
documentation
Sometimes it is possible that a library contains missing or incomplete type definitions. You could
either use // @ts-ignore
comments and live with it or you can write the type declaration yourself.
Create a *.d.ts
file somewhere in your src
folder and use the following syntax:
import 'package' // `'package'` is the library we want to extend
declare module 'package' {
// we re-declare the module
// we add the missing function or override the existing one
export declare function someFunction(): boolean
}
You can now use it inside your code:
import { someFunction } from 'package'
const result = someFunction()
// result: boolean
Whenever you are using a function that has no or not so good TypeScript
definitions or whenever
you need to cast something everytime you use a function, you should wrap it into a new function and
add type definitions there.
import { getContext } from 'svelte'
// per default the return type is `unknown`
const c1 = getContext('my context')
// we need to pass a generic to let `TypeScript` know what we expect
const c2 = getContext<string>('my context')
// oops typo!
const c3 = getContext<string>('my comtext')
The usage of getContext
in the example above has three issues:
- you need to specify the return type whenever you call the function, so you would need to check where the context gets set and copy the type definition from there
- when you refactor the context to hold different data, you need to update the type definition everywhere
- you could easily introduce a typo because the parameter of the function is typed as a generic string
We can eliminate the issues mentioned above by wrapping getContext
into a new function
import { getContext } from 'svelte'
const getMyContext = () => getContext<string>('my context')
const c = getMyContext() // typed as `string`
We now have a single function that is responsible for the type (1) and the context name (3).
And we use that function when we want to access the data. When refactoring (2) we only need to
change it in a single place (and let TypeScript
tell you if it's now getting used in a wrong way).
Some types may look similar to another type but they are not actually related. If you are working
with databases the ID
field would be such a case.
Typing the id of your DB model as a string
is probably not wrong because from a technical
perspective they are strings
. But it is also a string
for other models of your DB.
interface Product {
id: string
author: string
}
interface Category {
id: string
name: string
}
const book: Product = { id: '1', /* ...rest */ }
const category: Category = { id: '1', /* ...rest */ }
const findById(productId): Product | undefined = { /* implementation */ }
findById(product.id) // valid
findById(category.id) // we query the product DB with an id from the category DB
// this will probably always return `undefined` if your DB uses random IDs
You could introduce potential bugs if you are not careful. In the case above it probably is clear
because we have named the data variables clearly, but what if you name the variable just result
and then use the findById
function. At first glance it looks good, but it is actually wrong.
We can improve this by giving each model its unique ID type.
// in the next two lines we define an `opaque type` for our ProductId
declare const _productId: unique symbol
export type ProductId = string & { readonly [_productId]: never }
// we define our type as `string` but with additional meta-information
// that tell TypeScript that this is a unique string
// this type will behave like a `string`, so you can use it in functions that
// expect a `string`, but it doesn't work the other way around. You can't pass
// a normal `string` to a function that expects a certain `opaque type`
// of course you can use `opaque types` also for `numbers` and other types
interface Product {
id: ProductId
author: string
}
// we also define a CategoryId as an `opaque type`
declare const _categoryId: unique symbol
type CategoryId = string & { readonly [_categoryId]: never }
interface Category {
id: CategoryId
name: string
}
// in this case we need to cast it, because we are hardcoding the IDs
// in a real world scenario the data gets loaded on runtime
// from the DB and no casting is needed there
const book: Product = { id: '1' as ProductId, /* ...rest */ }
const category: Category = { id: '1' as CategoryId, /* ...rest */ }
const findById(id: ProductId): Product | undefined = { /* implementation */ }
findById(book.id) // valid
findById(category.id) // TypeScript shows an error:
// Argument of type 'CategoryId' is not assignable to parameter of type 'ProductId'
Now TypeScript
is able to tell you that something is wrong when you pass in the wrong ID.
This is not only useful for IDs but also for other string
types that e.g. have a special meaning.
Such examples could be:
LocalizedString
to make sure a Button component only accepts internationalized stringsSanitizedHtmlString
so you know it contains no potential unsafe charactersValidatedString
that tells you it's content was checked and marked as valid
Like you already have seen in the example from TS-tipp #4,
strings are really generic and can hold any kind of data. Luckily TypeScript
is flexible enough to
let us define which shape we expect the data.
// a list of possible options (`enum`-like)
type Options = 'A' | 'B' | 'C'
// `numbers` wrapped into a `string`
type NumberString = `${number}`
// really simple check if the format looks like an email
type EmailString = `${string}@${string}.${string}`
// simple check if it is an url
type LinkString = `http${'s' | ''}://${string}`
// simple check if it could be in the format of an IPv4 address
// note: we only can tell we expect any `number` but we cannot define value ranges yet
type IpStringSimple = `${number}.${number}.${number}.${number}`
// useful if you are handling multiple date formats from different apis
type Api1DateTimeString = `${number}-${number}-${number} ${number}:${number}:${number}`
// e.g. '2021-03-01 13:00:00'
type Api2Timestamp = `${number}` // e.g. '1635494400000'
// ... and many more potential use-cases
If we assign a value that doesn't match the definition of the shape TypeScript
will throw an
error.
You should now have a feeling what is possible with TypeScript
and how you can use it within your
Svelte
and SvelteKit
applications. You probably have learned something new about TypeScript
as
well. Just because the examples are listed here doesn't mean you need to type everything as
strict as possible. For some cases it makes sense to be stricter and sometimes beeing so strict will
introduce complexity you need to maintain over the lifetime of a project.
You probably don't need TypeScript
directly to profit from a strong type-checking experience
inside your Svelte
and SvelteKit
applications. VS Code
and the Svelte
extension can also
offer help if you annotate your components with JSDoc
comments.
Here is a simple example:
-
JSDoc:
<script> /** @type {string} */ export let name </script> Hello {name}!
-
TypeScript:
<script lang="ts"> export let name: string </script> Hello {name}!
It is up to you which syntax you prefer. Some parts of the SvelteKit
codebase are written in plain
JavaScript
files annotated with JSDoc
comments.
I would suggest directly using ´TypeScript´ because if you need more complex types, you will need to
write
declaration files
in TypeScript
syntax. It will be harder for you to write them if you are not used to the
TypeScript
syntax.
Also if you need to use generics inside your JSDoc
comments, you may find the syntax a bit messy:
- JSDoc:
/**
* @typedef { import('./my-types').Type1 } Type1,
* @typedef { import('./my-types').GenericType<Type1> } Type1GenericType,
*/
/**
* @param {Type1GenericType} param
* @returns {void}
*/
export const myFunction = (param) => {
// ... implementation
}
- TypeScript:
import type { Type1, GenericType } from ('./my-types')
export const myFunction = (param: GenericType<Type1>): void => {
// ... implementation
}
See the
official documentation to
learn more about the JSDoc
-syntax.
Become a sponsor ❤️ if you want to support my open source contributions.
Thanks for sponsoring my open source work!