Skip to content

Free MIT template · Day 6 of 365

You're looking at a demo. Download the built static files (drop on any host) or grab the full source to customize.

↓ Download static ↓ Source (Astro) All 365 →Build with me →

# TypeScript usage

routekit is written in TypeScript and ships its own `.d.ts` files. Every public API is fully typed. The one thing you have to do as a user: use `as const` on your route table.

## The `as const` dance

    const routes = [  { path: '/users/:userId',           handler: ... },  { path: '/posts/:slug/comments/:c', handler: ... },] as const;  // ← important

Without `as const`, TypeScript widens `path` from the string literal `'/users/:userId'` to plain `string`. Once that happens, routekit can’t pull the param names out — they’re gone.

With `as const`, the types are preserved, and:

    type Params = ExtractParams<typeof routes[0]['path']>;//   ^ { userId: string }

This shape powers the runtime `match.params` typing.

## `RouteDefinition`

    type RouteDefinition = {  readonly path: string;  readonly handler: () => Promise<unknown>;  readonly meta?: Record<string, unknown>;};

Routes are `readonly` so the type system can hold onto literal strings. If you build the table dynamically, the helper `defineRoute()` keeps the literals in scope:

    import { defineRoute } from 'routekit';
    const homeRoute = defineRoute('/', () => import('./pages/home'));//    ^ inferred as { path: '/'; handler: ...; }

You can compose:

    const routes = [  homeRoute,  defineRoute('/about', () => import('./pages/about')),] as const;

## `Match<T>`

`router.match()` and the `'navigate'` event return `Match<T>` keyed by your route table:

    type Match<T> = {  path: T[number]['path'];           // narrowed to the matched literal  url: string;  params: ParamsFor<T[number]['path']>;  meta: T[number]['meta'];  handler: () => Promise<unknown>;  state?: Record<string, unknown>;};

`path` is a discriminator. To read params with full type safety, narrow on it:

    router.on('navigate', ({ match }) => {  if (match.path === '/users/:userId') {    match.params.userId; // ← string    match.params.foo;    // ← compile error  }});

This is the same pattern as discriminated unions for Redux actions or `kind` fields on tagged unions.

## Generic helpers

    import type { RouteDefinition, Match, ParamsFor } from 'routekit';

Type | Purpose
--- | ---
`RouteDefinition` | Single route shape.
`Match<Routes>` | Router’s match output.
`ParamsFor<Path>` | Extract params from a literal path. `ParamsFor<'/users/:id'>` → `{ id: string }`.
`Router<Routes>` | The full router instance type.

## Wildcard typing

Wildcard routes type their captured segment as `wildcard: string`:

    const routes = [  { path: '/files/*', handler: ... },] as const;
    router.on('navigate', ({ match }) => {  if (match.path === '/files/*') {    match.params.wildcard; // ← string (e.g. 'a/b/c.png')  }});

## Strictness

routekit is built with `"strict": true` and `"noUncheckedIndexedAccess": true`. If your project uses these too, no surprises. If you’re on looser settings, the types will still work — but you may see fewer compile-time errors than the documentation implies.

Tip

If params look stringly-typed (`Record<string, string>`) and you expected the narrow form, the most common cause is a missing `as const`. Check that first.

## Generated types (forward-looking)

The future `routekit codegen` CLI will write a typed `RouteMap` so you can do:

    import { RouteMap } from './routekit.types';//                    ^ generated
    router.go<'/posts/:id'>({ id: '42' }); // typed call shape

Until the CLI ships, the `as const` table + `Match<typeof routes>` covers ~95% of what people need.