Type-safe router
This project is part of Literium WEB-framework but can be used standalone.
Why
The ordinary routers is not type-safe, so when you change route or corresponding interface you need track compliance between it manually.
The compiler cannot help you in cases of breaking that compliance.
For example, your app may expect arguments which actual router don't provide.
Or viceversa the router may require some arguments which missing in your application.
This leads us to a question: what can we do to make type system to keep the routes and corresponding interfaces in compliance?
Many frameworks in other languages with type inference, like Haskell and PureScript, solves this question. So why TypeScript cannot do same too.
How
Each route have corresponds object with typed fields i.e. arguments.
The routes without arguments corresponds to empty object.
The type of this arguments object is inferred automatically by constructing the route.
You can construct complex routes using simple routing algebra.
Routing algebra
Atomic routes
Route with static path-piece
In simplest case you can use path string to create routes.
import { dir, match, build } from 'literium-router';
const blog = dir('/blog');
match(blog, '/blog')
match(blog, '/blog/1')
match(blog, '/other')
build(blog, {})
build(blog, {id: 1})
This route corresponds to path /blog and empty object.
Route with argument
When you want handle some data in the path string you can create route with the typed argument.
import { num, arg, match, build } from 'literium-router';
const blog_id = arg({id: num});
match(blog_id, '1')
match(blog_id, 'other')
match(blog_id, 'blog/1')
build(blog_id, {})
build(blog_id, {id: 1})
The first argument of arg()
is the object with property name as key and with property type-tag as value.
The available type-tags and corresponding value types is defined by the second argument of arg()
.
Query string arguments
To deal with query string you can use query()
.
import { num, query, match, build } from 'literium-router';
const blog_posts = query({offset: num, length: num});
match(blog_posts, '?offset=10&length=5')
match(blog_posts, '?length=5&offset=10')
match(blog_posts, '?length=5')
build(blog_posts, {length:5})
build(blog_posts, {offset:10, length:5})
When you need process different query strings you can alternate queryes using variants.
Route combinators
Sequential
To build complex routes you can use seq()
to sequentially combine sub-routes.
import { num, dir, arg, seq, match, build } from 'literium-router';
const blog_by_id = seq(dir('/blog/'), arg({id: num});
match(blog_by_id, '/blog/1')
match(blog_by_id, '/blog/1/')
match(blog_by_id, '/other')
match(blog_by_id, '1')
build(blog_by_id, {})
build(blog_by_id, {id: 1})
Variants
And of course you can combine some number of routes which can be used alternatively.
import { str, num, dir, arg, seq, alt, match, build } from 'literium-router';
const blogs_by_tag = seq(dir('/blog/tag-'), arg({id: str}));
const by_author = seq(dir('/author-'), arg({user: num}));
const blogs_by_tag_opt_by_author = seq(blogs_by_tag, alt(by_author, dir('')));
match(blogs_by_tag_opt_by_author, '/blog/tag-js')
match(blogs_by_tag_opt_by_author, '/blog/tag-js/author-3')
match(blogs_by_tag_opt_by_author, '/blog/tag-js/')
match(blogs_by_tag_opt_by_author, '/blog/tag-js/author-')
match(blogs_by_tag_opt_by_author, '/other')
build(blogs_by_tag_opt_by_author, {})
build(blogs_by_tag_opt_by_author, {tag:"git"})
build(blogs_by_tag_opt_by_author, {user:3})
build(blogs_by_tag_opt_by_author, {tag:"git",user:3})
NOTE: Keep in mind the more complex variant must precede simple because in else case the simple routes may become unreachable to match.
This assumption is accepted in order to simplify and speedup route matching algorithm.
Custom route types
One of the more helpful feature of this router is the possibility to define your own argument types.
For example, suppose we have enum type Order which has two values: Asc and Desc. We can simply use it in our routes as arguments when we defined corresponding TypeApi.
import { dir, arg, seq, match, build, Route } from 'literium-router';
export const enum Order { Asc, Desc }
export const ord: Route<Order> = {
p: path => {
const m = path.match(/^(asc|desc)(.*)$/);
if (m) return [m[1] == 'asc' ? Order.Asc : Order.Desc, m[2]];
},
b: arg => arg === Order.Asc || arg === Order.Desc ?
`${arg == Order.Asc ? 'asc' : 'desc'}` : undefined
};
const blogs_by_date = seq(dir('/blog/date-'), arg({sort: ord}));
match(blogs_by_date, '/blog/date-asc')
match(blogs_by_date, '/blog/date-desc')
match(blogs_by_date, '/blog/date-')
match(blogs_by_date, '/blog/date-other')
build(blogs_by_date, {sort:Order.Asc})
build(blogs_by_date, {sort:Order.Desc})
build(blogs_by_date, {})
build(blogs_by_date, {sort:'asc'})
Router
Usually in real-life applications we don't like to operate with routes separately.
One of ways to works with all defined routes together is using of the methods matchs()
and builds()
.
This methods gets the object with routes names as keys and routes itself as values.
It operates with the complex state which represents object with routes names as keys and corresponding route arguments objects as values.
import { num, str, dir, arg, seq, matchs, builds } from 'literium-router';
const root = dir('/');
const blog_by_id = seq(root, dir('blog/'), arg({id:num}));
const blogs_by_tag = seq(root, dir('blog/tag-'), arg({tag:str}));
const routes = { root, blogs_by_tag, blog_by_id };
matchs(routes, '/')
matchs(routes, '/blog/2')
matchs(routes, '/blog/tag-es6')
builds(routes, {root:{}})
builds(routes, {blog_by_id:{id:2}})
builds(routes, {blogs_by_tag:{tag:"es6"}})
TypeScript 2.7 issue
The TypeScript 2.7.x have an issue which breaks the type inference of object values, so I strongly recommend to use TypeScript 2.6.x until TypeScript 2.8.x has been released.