Skip to content

Free MIT template. If it saved you time, you can tip the maker:

Support on Ko‑fi →

Want it built for your own brand? Send a brief, a mockup you already have, or start from scratch:

Ask about custom work →

Made by Allan Niñal

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';
TypePurpose
RouteDefinitionSingle 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.

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.