npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@itaylor/type-router

v1.0.2

Published

A lightweight, type-safe router for single-page applications with first-class TypeScript support

Readme

type-router

A lightweight, type-safe router for single-page applications with first-class TypeScript support. Zero dependencies, full type inference, and a simple but powerful API.

Features

  • 🎯 Robust Type-Safety: Full TypeScript support with automatic parameter type inference
  • 🪶 Lightweight: Zero dependencies, ~2KB minified
  • 🔄 Flexible Routing: Hash-based routing by default with history-based routing available
  • 🎨 Framework Agnostic: Works with any UI framework or vanilla JavaScript
  • 🪝 Lifecycle Hooks: Route enter/exit/change callbacks for fine-grained control
  • 📦 Simple API: Intuitive and minimal API surface
  • Fast: Efficient regex-based route matching
  • 🔀 Async Navigation: Consistent async behavior across routing modes
  • 🔍 Query Parameters: First-class support for type-safe query parameters

Installation

From JSR

# Deno
deno add @itaylor/type-router

# npm (use any of npx, yarn dlx, pnpm dlx, or bunx)
npx jsr add @itaylor/type-router

From npm

npm install @itaylor/type-router
# or
yarn add @itaylor/type-router
# or
pnpm add @itaylor/type-router

Quick Start

import { createRouter } from '@itaylor/type-router';

// Define your routes with path and query parameters
const router = createRouter(
  [
    { path: '/' },
    { path: '/about' },
    {
      path: '/user/:id',
      onEnter: (params) => {
        console.log(`Entering user profile: ${params.id}`);
        // params.id is typed as string!
      },
    },
    { path: '/post/:category/:slug' },
    {
      path: '/search?q&category&sort',
      onEnter: (params) => {
        console.log(`Search: ${params.q}, Category: ${params.category}`);
        // params.q, params.category, params.sort are typed as string | undefined
      },
    },
  ] as const,
);

// Navigate with type safety
await router.navigate('/');
await router.navigate('/about');
await router.navigate('/user/:id', { id: '123' });
await router.navigate('/user/123'); // Also works with concrete paths
await router.navigate('/post/:category/:slug', {
  category: 'tech',
  slug: 'intro-to-typescript',
});

// Navigate with query parameters
await router.navigate('/search?q=typescript&category=web&sort=recent');

// Or navigate with an object containing the query parameters
await router.navigate('/search', {
  q: 'typescript',
  category: 'web',
  sort: 'recent',
});
// TypeScript will catch these errors:
// router.navigate('/invalid-route');  // ❌ Type error!
// router.navigate('/user/:id', { wrong: '123' }); // ❌ Type error!
// router.navigate('/search?bomb=true'); // ❌ Type error!

Core Concepts

Routes

Routes are defined as objects with a path pattern and optional lifecycle hooks:

const router = createRouter(
  [
    {
      path: '/user/:id/posts/:postId',
      onEnter: (params) => {
        // Called when entering this route
        console.log(`User ${params.id}, Post ${params.postId}`);
      },
      onExit: (params) => {
        // Called when leaving this route
        console.log(`Leaving user ${params.id}`);
      },
      onParamChange: (params, prevParams) => {
        // Called when navigating to the same route with different params
        console.log(`User changed from ${prevParams.id} to ${params.id}`);
      },
    },
  ] as const,
);

Route Parameters

Parameters in routes are defined using the :paramName syntax. TypeScript automatically infers the parameter types:

const router = createRouter(
  [{ path: '/product/:category/:id' }] as const,
);

// TypeScript knows params must have 'category' and 'id'
router.navigate('/product/:category/:id', {
  category: 'electronics',
  id: 'laptop-123',
});

Query Parameters

Declare query parameters directly in the route path using standard query-string syntax. Parameters are optional by default, and you can opt into richer typing by annotating them inline:

const router = createRouter(
  [
    { path: '/search?q&category&sort' },
    {
      path: '/product/:id?color&size&variant',
    }, // Mixed path + query params
    {
      path: '/flags?fizz&buzz&active=bool&count=number&tags=string[]',
    }, // Typed query params
  ] as const,
);

// All query parameters are optional. Those without an explicit type stay `string | undefined`.
await router.navigate('/search?q=typescript&category=web');
await router.navigate('/search'); // Works without query params

// Mixed path and query parameters
await router.navigate('/product/laptop123?color=silver&size=15inch');

// Typed query parameters infer richer types:
await router.navigate(
  '/flags?fizz&buzz&active=bool&count=number&tags=string[]',
  {
    fizz: 'one', // string
    active: true, // boolean (defaults to false when omitted)
    count: 3, // number
    tags: ['alpha', 'beta'], // string[]
  },
);

// Boolean params accept '?flag', '?flag=true', or '?flag=false' – absence means false.

// Path parameters always take precedence over query parameters with same name
const router2 = createRouter([{ path: '/user/:id?id&settings' }] as const);
await router2.navigate('/user/alice?id=ignored&settings=dark');
// params.id === 'alice' (from path), not 'ignored' (from query)

Key principles:

  • Only declared query parameters are extracted - undeclared params are ignored
  • Query parameters don't affect route matching - only the path part (before ?) is used
  • Path parameters trump query parameters - when there's a name conflict, path params win
  • Type-safe and optional - query params default to string | undefined, but you can opt into richer typing with ?flag=bool, ?count=number, ?tags=string[], etc. Boolean params treat ?flag/?flag=true as true, ?flag=false as false, and absence as false.

Not a fan of the path syntax? Want more curly braces? Less colons? No ampersands? Something different?

You can create your own path syntax! See PATH_EXTRACTOR_ARCHITECTURE.md and custom-path-extractor.ts for a complete example. If you do it right, you can even preserve the same level of TypeScript compile time path checking we have with the default parameter extraction methods.

Navigation Modes

type-router supports two URL modes:

// Hash mode (default) - Works everywhere, no server config needed!
// URLs look like: #/path/to/route
const hashRouter = createRouter(routes); // Uses hash mode by default

// History mode - Clean URLs, requires server configuration
// URLs look like: /path/to/route
const historyRouter = createRouter(routes, {
  urlType: 'history',
});

Routing Modes

Hash Mode (Default) 🎯

Hash mode is the default because it alway works everywhere:

// Hash mode - no configuration needed!
const router = createRouter(routes); // Uses hash mode by default

Benefits:

  • Zero server setup - Works on GitHub Pages, Netlify, Vercel, S3, CDNs
  • No 404 errors - Users can directly visit yoursite.com/#/users/123
  • File:// URLs work - Perfect for local development and Electron apps
  • Instant deployment - Upload files anywhere and routing works
  • Great for demos - CodeSandbox, JSFiddle, static demos work immediately

URLs look like: https://yoursite.com/#/users/123

History Mode

For clean URLs when you have server control:

const router = createRouter(routes, {
  urlType: 'history',
});

Benefits:

  • Clean URLs - https://yoursite.com/users/123 (no hash)
  • SEO friendly - Traditional URL structure
  • Server-side rendering compatible

Requirements:

  • Server configuration needed - Must serve index.html for all routes
  • Hosting limitations - Not all static hosts support this easily

When to Use Each

Choose Hash Mode (default) when:

  • Building a client-only SPA
  • Using static hosting (GitHub Pages, Netlify, etc.)
  • Want zero configuration
  • Rapid prototyping or demos
  • Building desktop apps with Electron

Choose History Mode when:

  • Clean URLs are critical
  • You control the server
  • SEO is a primary concern

API Reference

createRouter(routes, options?)

Creates a new router instance.

Parameters

  • routes: An array of route definitions (use as const for best type inference)
  • options: Optional configuration object

Options

| Option | Type | Default | Description | | --------------- | --------------------- | ----------- | ------------------------------------------------------------------------------------------------------- | | urlType | 'hash' \| 'history' | 'hash' | Routing mode to use | | fallbackPath | string | undefined | Route to use when no match is found (must be a concrete, non-parameterized route path from your routes) | | autoInit | boolean | true | Automatically initialize routing on creation | | onEnter | function | undefined | Global hook called when entering any route | | onExit | function | undefined | Global hook called when exiting any route | | onParamChange | function | undefined | Global hook called when params change | | onMiss | function | undefined | Called when no route matches |

makeRoute(route)

Helper function that creates a route with proper TypeScript inference. While not strictly required, it provides better type checking when defining routes outside of the createRouter call.

import { makeRoute } from '@itaylor/type-router';

// Better type inference
const userRoute = makeRoute({
  path: '/user/:id',
  onEnter: (params) => {
    // params.id is properly typed as string
    console.log('User ID:', params.id);
  },
});

const router = createRouter([userRoute] as const);

Router Methods

navigate(path, params?)

Navigate to a route with compile-time type checking. Returns a Promise that resolves when navigation is complete.

// With parameters (pattern matching)
await router.navigate('/user/:id', { id: '123' });

// With concrete path
await router.navigate('/user/123');

// Trailing slashes are optional
await router.navigate('/about');
await router.navigate('/about/'); // Same as above

navigateAny(path)

Navigate to any string path without compile-time type checking. Useful for runtime navigation from user input, HTML links, or external URLs. Returns a Promise that resolves when navigation is complete.

// Navigate to any path - useful for dynamic/runtime navigation
await router.navigateAny('/dynamic/path/from/user/input');
await router.navigateAny(window.location.pathname); // Current URL

// No compile-time checking - will use fallback if route doesn't exist
await router.navigateAny('/potentially/invalid/route');

getState()

Get the current routing state. Returns an object with nullable properties.

const state = router.getState();

// State properties can be null if no route is active
console.log(state.path); // string | null - Current path
console.log(state.params); // Record<string, string> - Current parameters
console.log(state.route); // Route object | null - Current route

// Always check for null before using
if (state.route) {
  console.log('Current route:', state.route.path);
}

Note: The state types are unions of all possible routes, not strictly typed to the current route. Always check state.route for null and use type guards as needed.

subscribe(callback)

Subscribe to route changes. Returns an unsubscribe function.

const unsubscribe = router.subscribe((state) => {
  console.log('Route changed:', state.path);

  // Check for null route
  if (state.route) {
    console.log('Active route:', state.route.path);
  }
});

// Later...
unsubscribe();

computePath(pattern, params?)

Convert a route pattern and parameters into a concrete path. Works with both path and query parameters.

const path = router.computePath('/user/:id', { id: '123' });
console.log(path); // '/user/123'

// With query parameters
const searchPath = router.computePath('/search?q&category&sort', {
  q: 'typescript',
  category: 'web',
  sort: 'recent',
});
console.log(searchPath); // '/search?q=typescript&category=web&sort=recent'

// Mixed path and query parameters
const productPath = router.computePath('/product/:id?color&size', {
  id: 'laptop123',
  color: 'silver',
  // size omitted - won't appear in URL
});
console.log(productPath); // '/product/laptop123?color=silver'

// Works without parameters for routes without params
const homePath = router.computePath('/');
console.log(homePath); // '/'

init()

Manually initialize the router. Useful when autoInit is false.

const router = createRouter(routes, {
  autoInit: false,
});

// Later, when ready...
router.init();

Advanced Usage

Using makeRoute for Better Type Safety

When defining routes outside of createRouter, use makeRoute for better type inference:

import { createRouter, makeRoute } from '@itaylor/type-router';

// When routes are defined inline - no makeRoute needed
const inlineRouter = createRouter(
  [
    {
      path: '/user/:id',
      onEnter: (params) => {
        // params.id is properly inferred as string
        console.log(params.id);
      },
    },
  ] as const,
);

// When routes are defined separately - makeRoute helps with inference
const userRoute = makeRoute({
  path: '/user/:id',
  onEnter: (params) => {
    // params.id is properly inferred as string
    console.log(params.id);
  },
});

const separateRouter = createRouter([userRoute] as const);

Query Parameters

Query parameters provide a powerful way to add optional, type-safe parameters to your routes. They're declared directly in the route path and automatically inferred by TypeScript.

Enhanced Path-Only Navigation

Query parameters support three equivalent navigation patterns:

const router = createRouter(
  [{ path: '/product/:id?color&size&variant' }] as const,
);

// All three approaches are equivalent and type-safe:

// 1. Path template (enhanced)
await router.navigate('/product/:id', {
  id: 'phone456',
  color: 'blue',
  size: '6.1inch',
  variant: 'wifi',
});

// 2. Concrete path (enhanced)
await router.navigate('/product/12', {
  color: 'red',
  size: 'large',
  variant: 'premium',
});

// 3. Traditional (still works)
await router.navigate('/product/:id?color&size&variant', {
  id: '789',
  color: 'green',
});

Key Features

  • Path parameters are required (string) - extracted from URL path
  • Query parameters are optional (string | undefined) - declared after ?
  • Path parameters trump query parameters with the same name
  • URL encoding/decoding is handled automatically
  • Only declared parameters are extracted; others are ignored
// Mixed path and query parameters
const router = createRouter(
  [
    { path: '/search/:query?page&category&sort' },
  ] as const,
);

// TypeScript knows: query is required, others are optional
await router.navigate('/search/:query', {
  query: 'typescript', // string (required)
  page: '2', // string | undefined (optional)
  category: 'programming', // string | undefined (optional)
  // sort omitted - perfectly valid
});

Global vs Route-Level Hooks

You can define hooks at both global and route levels. Global hooks run before route-level hooks:

const router = createRouter(
  [
    {
      path: '/dashboard',
      onEnter: () => console.log('Route: Entering dashboard'),
    },
  ],
  {
    onEnter: (route, params) => {
      console.log('Global: Entering', route.path);
    },
  },
);

// Navigation will log:
// "Global: Entering /dashboard"
// "Route: Entering dashboard"

Fallback Routes and Error Handling

Handle unmatched routes gracefully with compile-time type safety:

const router = createRouter(
  [
    { path: '/' },
    { path: '/about' },
    { path: '/user/:id' }, // Parameterized route
    { path: '/404' },
  ] as const,
  {
    // ✅ Valid - fallback must be a concrete route from your routes
    fallbackPath: '/404',

    // ❌ TypeScript error - fallback cannot be parameterized
    // fallbackPath: "/user/:id",

    // ❌ TypeScript error - fallback must exist in routes
    // fallbackPath: "/nonexistent",

    onMiss: (path) => {
      console.log(`No route found for: ${path}, redirecting to 404`);
    },
  },
);

// Use navigateAny for potentially invalid routes
await router.navigateAny('/some/unknown/path'); // Will use fallback

Fallback Path Constraints

The fallbackPath option has compile-time type checking with these rules:

  1. Must exist in your routes: TypeScript ensures the fallback path matches one of your defined routes
  2. Cannot be parameterized: Fallback paths must be concrete (no :param segments)
  3. Type safety: Prevents typos and ensures fallback routes are actually available
// ✅ Valid fallback configurations
const router1 = createRouter(
  [
    { path: '/' },
    { path: '/404' },
  ] as const,
  {
    fallbackPath: '/', // Concrete route that exists
  },
);

const router2 = createRouter(
  [
    { path: '/home' },
    { path: '/error' },
  ] as const,
  {
    fallbackPath: '/error', // Another concrete route
  },
);

// ❌ Invalid - TypeScript will catch these
const router3 = createRouter(
  [
    { path: '/user/:id' },
  ] as const,
  {
    // fallbackPath: "/user/:id" // Error: Cannot use parameterized route
    // fallbackPath: "/missing" // Error: Route doesn't exist
  },
);

Runtime vs Compile-time Navigation

Choose the right navigation method for your use case:

const router = createRouter(
  [
    { path: '/user/:id' },
    { path: '/search/:query' },
  ] as const,
);

// Compile-time checked - preferred for known routes
await router.navigate('/user/:id', { id: '123' });

// Runtime navigation - use for dynamic scenarios
const userInput = '/search/typescript';
await router.navigateAny(userInput);

// Click handler example
document.addEventListener('click', async (e) => {
  const link = e.target.closest('[data-route]');
  if (link) {
    e.preventDefault();
    await router.navigateAny(link.getAttribute('href'));
  }
});

Delayed Initialization

Sometimes you need to set up your app before activating routing:

const router = createRouter(routes, {
  autoInit: false,
});

// Do some async setup...
await loadUserData();
await initializeApp();

// Now initialize routing
router.init();

Parameter Changes

When navigating to the same route with different parameters, only onParamChange is called:

const router = createRouter(
  [
    {
      path: '/user/:id',
      onEnter: (params) => {
        console.log('Setting up user view');
      },
      onParamChange: (params, prevParams) => {
        console.log(`User changed from ${prevParams.id} to ${params.id}`);
      },
      onExit: () => {
        console.log('Cleaning up user view');
      },
    },
  ] as const,
);

await router.navigate('/user/:id', { id: '1' }); // Calls onEnter
await router.navigate('/user/:id', { id: '2' }); // Calls onParamChange only
await router.navigate('/'); // Calls onExit

TypeScript Benefits

type-router provides exceptional TypeScript support:

Type-Safe Navigation

const router = createRouter(
  [
    { path: '/user/:userId' },
    { path: '/post/:category/:id' },
  ] as const,
);

// ✅ All valid
router.navigate('/user/:userId', { userId: '123' });
router.navigate('/user/123');
router.navigate('/post/tech/typescript-intro');

// With query parameters
router.navigateAny('/search?q=typescript&category=web');

// ❌ TypeScript errors
router.navigate('/unknown'); // Unknown route
router.navigate('/user/:userId', { id: '123' }); // Wrong param name
router.navigate('/post/:category'); // Missing param

Inferred Parameter Types

const router = createRouter(
  [
    {
      path: '/article/:year/:month/:slug',
      onEnter: (params) => {
        // TypeScript knows params has year, month, and slug properties
        const { year, month, slug } = params; // All typed as string
      },
    },
    {
      path: '/search/:query?page&tags&exact',
      onEnter: (params) => {
        // Path parameters are required (string)
        const query: string = params.query;

        // Query parameters are optional (string | undefined)
        const page: string | undefined = params.page;
        const tags: string | undefined = params.tags;
        const exact: string | undefined = params.exact;
      },
    },
  ] as const,
);

Error Handling

type-router throws errors for invalid paths and missing routes:

// Invalid paths (double slashes) throw immediately
try {
  await router.navigate('//invalid//path');
} catch (error) {
  console.error('Invalid path:', error.message);
}

// Unknown routes without fallback throw errors
try {
  await router.navigate('/unknown');
} catch (error) {
  console.error('Route not found:', error.message);
}

// Use navigateAny with fallback for safe navigation
const router = createRouter(routes, {
  fallbackPath: '/404',
  onMiss: (path) => console.log(`Redirecting ${path} to 404`),
});

await router.navigateAny('/unknown'); // Safe - uses fallback

License

MIT