ts-typed-routes

Zero dependency strongly typed routes and formatting for TypeScript

Usage no npm install needed!

<script type="module">
  import tsTypedRoutes from 'https://cdn.skypack.dev/ts-typed-routes';
</script>

README

ts-typed-routes

Zero dependency strongly typed routes and formatting for TypeScript.

This library exposes simple functions to help build routes with parameters while preserving strongly typed information and parsers.

This is a TypeScript project, although the compiled source is available in the npm package without any required runtime dependencies for vanilla JavaScript.

Examples

Importing

import { optionalParameter, parameter, route } from 'ts-typed-routes';

Creating routes

const dashboardRoute = route('root', 'user', 'settings);

dashboardRoute.path(); // === 'root/user/settings'

Parameters in routes

The true strength of this library is in modeling parameters and preserving their type information via TypeScript generics.

const userProfileRoute = route('profile', parameter('id'), parameter('tab'));

userProfileRoute.path(); // === 'profile/:id/:tab'
userProfileRoute.format({ // === 'user/JohnDoe1337/activity'
  id: 'JohnDoe1337',
  tab: 'activity',
});
/*
Note, the above format() function is typed using TypeScript,
and requires the following first arg: `{ id: string; tab: string }`
*/

Extending Routes

Oftentimes we'll want to build hierarchical paths by extending parent routes.

// we want to have the routes 'user/:name' and 'user/:name/friends/:page?'

const userRoute = route('user', parameter('id'));
const userFriendsRoute = userRoute.extend('friends', optionalParameter('page', Number));

// required arg type is { name: string }
route.format({ name: 'alice' }); // === 'user/alice'

// required arg type is { name: string, page: number }
route.format({ name: 'alice', page: 1 }); // 'user/alice/friends/1'

Typed parameters

You can supply an optional function that takes a string and returns the actual type you want for that parameter. This is handy using built in type functions such as Number and Boolean to clearly indicate to other developers what the type of that parameter should be.

const blogRoute = route('blog', 'posts/', parameter('index', Number));

blogRoute.path(); // === 'blog/posts/:index'

// required arg type is { index: number }
blogRoute.format({ index: 5 }); // === 'blog/posts/5'

Optional parameters

Parameters can be marked as optional as well. These parameters are not required when using .format(), and will be filled in with their default value.

const homeRoute = route('home', 'information', optionalParameter('activeTab'));

homeRoute.format({}); // === 'home/information'
homeRoute.format({ activeTab: 'activity' }); // === 'home/information/activity'

Custom types parameters

For complex types that cannot be easily serialized from a string you can supply a decoder function too.

type WeirdObject = {
  weird?: boolean;
  stuff: string;
};

const toWeirdObject = (str: string) => JSON.parse(str) as WeirdObject;
const fromWeirdObject = (obj: WeirdObject) => JSON.stringify(obj);

const weirdObjectRoute = route('store', parameter('weirdObject', toWeirdObject, fromWeirdObject));

weirdObjectRoute.path(); // === 'store/:weirdObject'
weirdObjectRoute.format({ weirdObject: {
    weird: true,
    stuff: 'something',
}}); // === 'store/%7B%22weird%22%3Atrue%2C%22object%22%3A%22something%22%7D'
// Note: the above path is a url encoded JSON string, hence so many escaped characters

Parsing route data

Many frameworks expose url parameters via path-to-regexp that that will return the parameters in a solely key based key/value object. You can then parse them into their correct types with this library.

const noStringsRoute = route(parameter('start', Number), optionalParameter('end', Boolean));

noStringsRoute.format({ start: 1337, end: true }); // === '1337/true

noStringsRoute.parse({ start: '777', end: 'true' }); // === { start: 777, end: true }
noStringsRoute.parse({ start: '-50' }); // === { start: -50, end: false }

Advanced Usage

These are more niche usage case examples that most developers probably will not need.

Custom joiners

The default character used to build the path is /, as this library is most useful to build url paths. However you can specific a custom string instead.

const simpleRoute = route('first', 'second', 'third');

simpleRoute.path({ joiner: '_-_' }); // === 'first_-_second_-_third'
simpleRoute.format({}, { joiner: '' }); // === 'firstsecondthird'

Encoders and decoders

By default, any values after being stringified, and before being parsed, will be passed through an encoder and decoder function. These default to encodeURIComponent and decodeURIComponent respectively.

Encoder

const example = route('start', parameter('val'));

// by default this URI encodes your values
example.format({ val: '$

 }); // === 'start/%24%24%24'

// you can override this with a custom string encoder
example.format({ val: '$

 }, { encoder: (str) => str }); // === start/$$

Decoder

const test = route('contact', parameter('email'));

const email = 'john%2Bdoe%40email.com'; // encoded
test.parse({ email }); // === { email: 'john+doe@email.com' }
test.parse({ email }, { decoder: (s) => s.toUpperCase() }); // === { email: 'JOHN%2BDOE%40EMAIL.COM' }

Parsing incorrect objects

const lotsOfParams = route(parameter('foo'), parameter('bar'), parameter('baz'));

const weirdData = { bar: 'something' } as Record<string, string>;
lotsOfParams.parse(weirdData, { useDefaults: true }); // === { foo: '', bar: 'something', baz: '' }
lotsOfParams.parse({}, { useDefaults: true }); // === { foo: '', bar: '', baz: '' }

React-Router

NOTE: This feature is still very experimental.

react-router-dom is listed as an optional dependency with this package.

If you are using it, then you can import a part of this package which adds useful react-router functionality. Otherwise you can ignore this feature.

importing

If you try to import ts-typed-routes/react-router without react-router-dom installed it will throw an Error.

However, if you do have it installed this is an extension library that exports helpful React components wrapped with route() logic.

import { reactRoute, parameter } from 'ts-typed-routes/react-router';

Note: The react-router file within this module re-exports all the imports from the index.

Basic usage

A ReactRoute exposes all the same functions as a basic Route, but adds some react-route helpers.

const simple = reactRoute('simple', 'route');

simple.path(); // === 'simple/route';

<Link>

const BlogRoute = reactRoute('blog', 'articles');

const BlogLinkText = () => (
  <span>
    Link to <blogRoute.Link>blog</blogRoute.Link>
  </span>
);

// another way to use Link

const { Link: BlogLink } = BlogRoute;
const BlobLinkText2 = () => <BlogLink>link text</BlogLink>;

// NavLink too
const BlogNavLink = BlogRoute.NavLink;

with parameters

const ArticleRoute = reactRoute('article', parameter('id'));

const Articles = (
  <>
    <ArticleRoute.Link parameters={{ id: 'cool-cars' }}>Cool cars</<ArticleRoute.Link>
    <ArticleRoute.Link parameters={{ id: 'generic-top-10' }}>Generic top 10 list</<ArticleRoute.Link>
  </>
)

<Route>

const pageRoute = reactRoute('page', parameter('num', Number));

const Page = (props: { num: number }) => (
  <section>
    <h1>Page {props.num}</h1>
    <p>Some generic page content</p>
  </section>
)

// this will render on page/1, page/2, etc
const PageRouter = (
  <pageRoute.Route render={(props) => <Page num={props.match.params.num} />} />
);

useParams

// have this route used somewhere in a react-router <Switch>
export const route = reactRoute('blog', parameter('postId'), optionalParameter('page', Number));

const Component = () => {
  const { postId, page } = route.useParams();

  return <BlogPost postId={postId} page={page} />;
}