@caseyplummer/ts-route
v1.0.6
Published
Route definitions in TypeScript
Maintainers
Readme
ts-route
Type-safe route definitions and navigation utilities for TypeScript applications.
Features
- 🎯 Type-safe route definitions with TypeScript generics
- 🔗 Dynamic parameter extraction from URL patterns
- 📝 Query parameter parsing with validation
- 🌳 Nested route support with breadcrumb trails
- 🔄 Dual module support (ESM + CommonJS)
- ⚡ Zero runtime dependencies
- 📦 Tree-shakeable exports
- ✨ Route defaults with app-specific configuration
- 🔧 Custom encoders for app-specific data types
- 📊 Enhanced query parsing for app-specific data types
- 🧪 Comprehensive testing with 320+ test cases
Installation
npm install @caseyplummer/ts-route
# or
pnpm add @caseyplummer/ts-route
# or
yarn add @caseyplummer/ts-routeQuick Start
1. Define Your Routes
import { Route as RouteBase, WildcardRoute } from '@caseyplummer/ts-route';
import { getHref } from './app-helpers';
import { AppQueryParams } from './app-query';
import { applyAppRouteDefaults } from './app-route-defaults';
// Define route paths as enums for type safety
export enum RoutePath {
Home = '',
Register = 'register',
SignIn = 'sign-in',
Dashboard = '@[handle]',
Profile = '@[handle]/profile',
Posts = 'posts',
Post = 'post/[id]',
}
// Query types for specific route(s)
export interface SignInQuery {
redirect?: string;
error?: string;
}
// Metadata types for specific route(s)
export interface SidebarMeta {
hasSidebar: boolean;
}
// Context types for specific route(s)
export interface IdNameContext {
id: string;
name: string;
}
// Narrow the Route type with an app default
export type Route<
Path extends RoutePath,
Query extends object = object,
Meta extends object = object,
Context extends object = object,
> = RouteBase<Path, AppQueryParams, Query, Meta, Context>;
// Define route types with full type safety
export type HomeRoute = Route<RoutePath.Home>;
export type RegisterRoute = Route<RoutePath.Register>;
export type SignInRoute = Route<RoutePath.SignIn, SignInQuery>;
export type DashboardRoute = Route<RoutePath.Dashboard>;
export type ProfileRoute = Route<RoutePath.Profile>;
export type PostsRoute = Route<RoutePath.Posts>;
export type PostRoute = Route<RoutePath.Post, object, object, IdNameContext>;
// Union type for all defined routes
export type AppRoute = HomeRoute | RegisterRoute | SignInRoute | DashboardRoute | ProfileRoute | PostsRoute | PostRoute;
// Centralized route definitions
export const baseRoutes: AppRoute[] = [
{
path: RoutePath.Home,
title: () => 'Home',
},
{
path: RoutePath.Dashboard,
title: ({ params }) => `${params?.handle}'s Dashboard`,
},
{
path: RoutePath.Profile,
parentPath: RoutePath.Dashboard,
title: ({ params }) => `${params?.handle}'s Profile`,
},
{
path: RoutePath.SignIn,
title: () => 'Sign In',
getQuery: (params) => ({
redirect: params.value('redirect'),
error: params.value('error'),
}),
},
{
path: RoutePath.Register,
title: () => 'Register',
},
{
path: RoutePath.Posts,
title: () => 'Posts',
},
{
path: RoutePath.Post,
title: ({ params, context }) => context?.name ?? `Post ID ${params?.id}`,
breadcrumb: ({ params, context }) => context?.name ?? `Post ID ${params?.id}`,
href: ({ params, context }) => {
const base = `/posts/${params?.id}`;
return context?.name ? `${base}#${context.name}` : base;
},
},
] as const;
export const appRoutes: AppRoute[] = applyAppRouteDefaults(baseRoutes);
// Provides better syntax for using routes in components
export const routes = {
home: { href: () => getHref(RoutePath.Home) },
dashboard: {
href: (handle: string) => getHref(RoutePath.Dashboard, { params: { handle } }),
},
profile: {
href: (handle: string) => getHref(RoutePath.Dashboard, { params: { handle } }),
},
signIn: {
href: (redirect?: string, error?: string) => getHref(RoutePath.SignIn, { query: { redirect, error } }),
},
register: { href: () => getHref(RoutePath.Register, {}) },
posts: { href: () => getHref(RoutePath.Posts) },
post: {
href: (id: string, name?: string) => getHref(RoutePath.Post, { context: { id, name } }),
},
};2. Use Routes in Components
React Example
import { Link, useLocation } from 'react-router-dom';
import { findRoute } from '@caseyplummer/ts-route';
import { appRoutes, routes } from './routes';
function Navigation() {
const location = useLocation();
const currentUser = { profile: { handle: 'john-doe' } }; // AuthState type
// Find the current route with full type safety
const currentRoute = findRoute(location.pathname, appRoutes);
return (
<nav>
{/* Clean, readable syntax */}
<Link to={routes.home.href()}>Home</Link>
<Link to={routes.dashboard.href(currentUser)}>Dashboard</Link>
<Link to={routes.account.href(currentUser)}>Account</Link>
<Link to={routes.profile.href(currentUser)}>Profile</Link>
<Link to={routes.posts.href()}>Posts</Link>
<Link to={routes.post.href('123', 'My Post')}>View Post</Link>
{/* Auth routes with query parameters */}
<Link to={routes.signIn.href()}>Sign In</Link>
<Link to={routes.verifyEmail.href(true)}>Verify Email</Link>
<Link to={routes.forgotPassword.href('[email protected]')}>Forgot Password</Link>
{/* Display current page title */}
{currentRoute && <h1>{currentRoute.title({})}</h1>}
</nav>
);
}Vue Example
<template>
<nav>
<router-link :to="routes.home.href()">Home</router-link>
<router-link :to="routes.dashboard.href(currentUser)">Dashboard</router-link>
<router-link :to="routes.account.href(currentUser)">Account</router-link>
<router-link :to="routes.profile.href(currentUser)">Profile</router-link>
<router-link :to="routes.posts.href()">Posts</router-link>
<router-link :to="routes.post.href('123', 'My Post')">View Post</router-link>
<h1 v-if="currentTitle">{{ currentTitle }}</h1>
</nav>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { findRoute } from '@caseyplummer/ts-route';
import { appRoutes, routes } from './routes';
const route = useRoute();
const currentUser = { profile: { handle: 'john-doe' } }; // AuthState type
const currentTitle = computed(() => {
const matched = findRoute(route.path, appRoutes);
return matched?.title({});
});
</script>Svelte Example
<script lang="ts">
import { findRoute } from "@caseyplummer/ts-route";
import { appRoutes, routes } from "./routes";
import { page } from "$app/stores";
let currentUser = { profile: { handle: "john-doe" } }; // AuthState type
let currentRoute = $derived(findRoute($page.url.pathname, appRoutes));
</script>
<nav>
<p><a href={routes.home.href()}>Home</a></p>
<p><a href={routes.dashboard.href(currentUser)}>Go to Dashboard</a></p>
<p><a href={routes.account.href(currentUser)}>Go to Account</a></p>
<p><a href={routes.profile.href(currentUser)}>Go to Profile</a></p>
<p><a href={routes.posts.href()}>View Posts</a></p>
<p><a href={routes.post.href("123", "My Post")}>View Post</a></p>
{#if currentRoute}
<h1>{currentRoute.title({})}</h1>
{/if}
</nav>Vanilla JavaScript/TypeScript
import { findRoute } from '@caseyplummer/ts-route';
import { appRoutes, routes } from './routes';
// Find current route
const currentPath = window.location.pathname;
const currentRoute = findRoute(currentPath, appRoutes);
if (currentRoute) {
console.log('Current page:', currentRoute.title({}));
console.log('Route params:', currentRoute.params);
console.log('Query params:', currentRoute.query);
}
// Navigate programmatically with different parameter types
function navigateToDashboard(user: AuthState) {
window.location.href = routes.dashboard.href(user);
}
function navigateToPost(id: string, name?: string) {
window.location.href = routes.post.href(id, name);
}
// Build URLs for links with various parameter combinations
document.querySelector('#posts-link')?.setAttribute('href', routes.posts.href());
document.querySelector('#post-link')?.setAttribute('href', routes.post.href('123', 'My Post'));
document.querySelector('#sign-in-link')?.setAttribute('href', routes.signIn.href());
document.querySelector('#verify-email-link')?.setAttribute('href', routes.verifyEmail.href(true));3. Advanced Features
Route Defaults
import { applyRouteDefaults } from '@caseyplummer/ts-route';
// Apply defaults to all routes
const appRoutes = applyRouteDefaults(baseRoutes, {
queryParamsFactory: (raw) => new AppQueryParams(raw),
encodeQueryValue: (value) => String(value),
serializeQuery: (query, args) => buildQueryString(query),
});Custom Value Encoding
import { encodeValue } from '@caseyplummer/ts-route';
// Custom encoding for your app's data types
function appEncodeValue(value: unknown): string {
if (value instanceof Date) {
return encodeURIComponent(value.toISOString());
}
return encodeValue(value);
}Enhanced Query Parameter Parsing
import { QueryParamsBase } from '@caseyplummer/ts-route';
class AppQueryParams extends QueryParamsBase {
// Parse dates from ISO strings
date(key: string): Date | undefined {
const value = this.value(key);
return value ? new Date(value) : undefined;
}
// Parse enums with case-insensitive matching
enumValue<T>(enumObj: T, key: string): T[keyof T] | undefined {
const value = this.value(key);
return this.findEnumValue(enumObj, value);
}
// Parse boolean values
boolean(key: string): boolean | undefined {
const value = this.value(key);
if (value === 'true') return true;
if (value === 'false') return false;
return undefined;
}
// Custom parsing for your app's query parameters
userId(): number | undefined {
const value = this.value('userId');
return value ? parseInt(value, 10) : undefined;
}
tags(): string[] {
return this.values('tag'); // Handles multiple values
}
}Nested Routes with Breadcrumbs
import { buildBreadcrumbTrail } from '@caseyplummer/ts-route';
// Build breadcrumb navigation
const breadcrumbs = buildBreadcrumbTrail('/john-doe/profile/settings', appRoutes);
// breadcrumbs = ['Home', "john-doe's Dashboard", "john-doe's Profile", 'Settings']API Reference
Core Types
Route<Path, Query, Meta, Context>- Route definition interfaceMatchedRoute<TRoute>- Result of route matchingRouteArgs<TRoute>- Arguments for route functions
Main Functions
findRoute(url, routes)- Find matching route for a URLbuildHref(route, args)- Build href for a route with parametersparseUrl(url, path)- Parse URL against a specific route pathbuildBreadcrumbTrail(url, routes)- Build breadcrumb navigationapplyRouteDefaults(routes, options)- Apply route defaultsserializeQuery(query, args, route)- Custom query serialization
Query Parameter Utilities
QueryParamsBase- Base class for custom query parameter parsing
Examples
Check out the examples/ directory for complete working examples:
app-routes.ts- Full route definitions with type safetyapp-helpers.ts- Custom helper functionsapp-query.ts- Extended query parameter parsingapp-encoders.ts- Custom value encodingapp-route-defaults.ts- Framework-level defaultsapp-validation.ts- Route validation utilities
License
MIT
