← Back to DevBytes

Svelte TypeScript: Strongly Typed Applications

Svelte TypeScript: Strongly Typed Applications

TypeScript has become the standard for building robust JavaScript applications, and Svelte's first-class TypeScript support makes it easier than ever to build strongly typed reactive UIs. In this tutorial, you'll learn what Svelte TypeScript is, why it matters, how to configure it, and how to apply best practices across components, stores, and APIs.

What Is Svelte TypeScript?

Svelte TypeScript refers to the ability to write Svelte components and application logic using TypeScript's static type system. The Svelte compiler understands .ts files natively, and the <script lang="ts"> directive enables TypeScript inside .svelte single-file components. This gives you compile-time type checking, intelligent autocompletion, and safer refactors without sacrificing Svelte's signature simplicity.

Under the hood, Svelte uses svelte-preprocess (or the built-in Vite plugin in newer SvelteKit versions) to transpile TypeScript before the compiler runs. The official svelte-check tool performs full type diagnostics on your components, similar to tsc but aware of Svelte's template syntax.

Why It Matters

Setting Up a Typed Svelte Project

The fastest way to start is with the official SvelteKit scaffolding tool, which includes TypeScript by default:

npm create svelte@latest my-app
# Choose: Skeleton project, TypeScript syntax
cd my-app
npm install

If you're working with a plain Svelte + Vite project, install the required dependencies:

npm install --save-dev typescript svelte-check @tsconfig/svelte

Create a tsconfig.json at the project root:

{
  "extends": "@tsconfig/svelte/tsconfig.json",
  "compilerOptions": {
    "strict": true,
    "moduleResolution": "bundler",
    "target": "ESNext",
    "module": "ESNext",
    "lib": ["ESNext", "DOM", "DOM.Iterable"],
    "types": ["svelte", "vite/client"]
  },
  "include": ["src/**/*.ts", "src/**/*.svelte"]
}

Add a type-check script to package.json:

{
  "scripts": {
    "check": "svelte-check --tsconfig ./tsconfig.json"
  }
}

Typed Components

To enable TypeScript inside a component, add lang="ts" to the script tag. Props are declared with the $props() rune (Svelte 5) or the export let syntax (Svelte 4). Here's a Svelte 5 example:

<!-- UserCard.svelte -->
<script lang="ts">
  interface User {
    id: number;
    name: string;
    email: string;
    role: 'admin' | 'editor' | 'viewer';
  }

  interface Props {
    user: User;
    onSelect?: (user: User) => void;
  }

  let { user, onSelect }: Props = $props();
</script>

<article>
  <h2>{user.name}</h2>
  <p>{user.email}</p>
  <span class="badge">{user.role}</span>
  <button onclick={() => onSelect?.(user)}>Select</button>
</article>

For Svelte 4, the equivalent looks like this:

<script lang="ts">
  export let user: User;
  export let onSelect: (user: User) => void = () => {};
</script>

Typed Stores

Svelte stores are generic. You can pass a type argument to writable, readable, and derived to lock down the value type:

// stores/session.ts
import { writable, derived } from 'svelte/store';

export interface Session {
  token: string | null;
  userId: number | null;
}

export const session = writable<Session>({
  token: null,
  userId: null
});

export const isLoggedIn = derived(session, ($s) => $s.token !== null);

Consuming the store in a component gives you a typed $session auto-subscription:

<script lang="ts">
  import { session, isLoggedIn } from './stores/session';
</script>

{#if $isLoggedIn}
  <p>Welcome, user #{$session.userId}</p>
{/if}

Typed Event Handlers

DOM event handlers benefit from TypeScript's Event subtypes. Use the correct event type to access specific properties safely:

<script lang="ts">
  function handleSubmit(event: SubmitEvent) {
    event.preventDefault();
    const form = event.target as HTMLFormElement;
    const data = new FormData(form);
    console.log(data.get('email'));
  }

  function handleInput(event: KeyboardEvent) {
    const target = event.currentTarget as HTMLInputElement;
    console.log(target.value);
  }
</script>

<form onsubmit={handleSubmit}>
  <input type="email" name="email" onkeydown={handleInput} />
  <button type="submit">Save</button>
</form>

Typed Component Events (Svelte 4)

In Svelte 4, you can type dispatched events using createEventDispatcher with a generic interface:

<script lang="ts">
  import { createEventDispatcher } from 'svelte';

  interface Item {
    id: number;
    label: string;
  }

  const dispatch = createEventDispatcher<{
    select: Item;
    delete: number;
  }>();

  function select(item: Item) {
    dispatch('select', item);
  }
</script>

In Svelte 5, prefer callback props (as shown in the UserCard example) over event dispatching for stronger typing and simpler APIs.

Typed API Calls

Define response types for your API layer so components receive well-shaped data:

// api/products.ts
export interface Product {
  id: number;
  name: string;
  price: number;
  inStock: boolean;
}

export async function fetchProducts(): Promise<Product[]> {
  const res = await fetch('/api/products');
  if (!res.ok) {
    throw new Error(`Failed to load products: ${res.status}`);
  }
  return res.json() as Promise<Product[]>;
}
<!-- ProductList.svelte -->
<script lang="ts">
  import { onMount } from 'svelte';
  import { fetchProducts, type Product } from './api/products';

  let products: Product[] = $state([]);
  let loading = $state(true);

  onMount(async () => {
    products = await fetchProducts();
    loading = false;
  });
</script>

{#if loading}
  <p>Loading…</p>
{:else}
  <ul>
    {#each products as product (product.id)}
      <li>{product.name} — ${product.price.toFixed(2)}</li>
    {/each}
  </ul>
{/if}

Working with SvelteKit Load Functions

SvelteKit's load functions are fully typed. Use the PageLoad and LayoutLoad types from $types, which are auto-generated based on your routes:

// src/routes/products/+page.ts
import type { PageLoad } from './$types';
import { fetchProducts } from '$lib/api/products';

export const load: PageLoad = async () => {
  const products = await fetchProducts();
  return { products };
};
<!-- src/routes/products/+page.svelte -->
<script lang="ts">
  import type { PageData } from './$types';

  let { data }: { data: PageData } = $props();
</script>

<ul>
  {#each data.products as product (product.id)}
    <li>{product.name}</li>
  {/each}
</ul>

Best Practices

Using satisfies for Safer Config

const theme = {
  primary: '#3498db',
  danger: '#e74c3c'
} satisfies Record<string, string>;

// theme.primary is still inferred as string, but the object
// is guaranteed to match the Record shape.

Conclusion

TypeScript transforms Svelte from a delightful prototyping tool into a production-grade framework suitable for large, long-lived applications. By typing your props, stores, event handlers, and API responses, you gain compile-time safety and a significantly better developer experience without giving up Svelte's concise syntax. Start with strict mode enabled, run svelte-check regularly, and co-locate your types so they evolve alongside your features. With these foundations in place, your Svelte applications will be easier to refactor, simpler to onboard, and far more resilient to runtime errors.

🛠 Tools from DevBytes

Inventory Tracker Pro — Excel inventory system, low-stock alerts · $19
AI Dev Kit for Mac — local AI dev environment templates · $9.99
KeyMapper for Mac — custom keyboard shortcut toolkit · $7.99

← Back to all articles