Type Safe Routing for Vue.js

Vue router is elegant and type safe enough for most use cases. But I want to experiment how far typescript can help us.

Typesafe routing isn’t easy to do.

The easiest way to declare multiple class and instantatiate their paramater type. For example, typed-url. But this is too verbose.

Another approach is to use functional combinator. To put it simple, combinator is high order function that can abstract various operation. Routing combinators usually are a bunch of functions that can accept strings as static url segment or a function as dynamic url parameter. Both purescript and swift. But monad is too monad-y. My head just explodes.

One unique way to provide type routing is using reified generic! A demo video has illustrted how to implment it.(Spoiler: for a function with type A => Response, one can access the class by A.type and cast value by guard let param: A = .... in swift. Whoa, reification is powerful). Github repo is here: https://github.com/NougatFramework/Switchboard

Compile time reflection is ideal for routing thing. Yesod uses template haskell to do this, Example. Macro paradise!

Scala has yet another unique construct called pattern match. Tumblr’s colossus is a great example to use pattern match for type safe routing.

And of course, haskell has many type safe routing library. Check out the review for more info.

JavaScript does not have powerful constructs like macro/pattern match. Combinator is the only way to achieve type safety but for client side component based routing, declaring more functions solely for routing doesn’t feel natural. And specifically TypeScript is still too feeble to describe routing. However, by combining tagged template, function overloading (or fun-dep), and intersection type (or row polymorphism), we can still do some interesting thing. If this were written in flow-type, more interesting thing could happen.

Frankly type safety in router does not grant you much: it cannot check tempalte code, it can only help you to double check the shape of parameters in $route. It can help you to type router instance better, but requires all routes to have a name field.

This is only a sketch for type safe routing design. Useful? No. Concise? Partly. Safety outweighs ease of use? No. Maybe it’s only suitable for type safe paranoid.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
interface Route<T> {
<K extends string, C>(c: StaticRouteConfig<K, C>): Routes<StaticRouteAct<K> & C & T>
<K extends string, R, P extends R, C>(c: RouteConfig<K, R, P, C>): Routes<RouteAct<K, P> & C & T>
}

interface StaticRouteAct<K> {
(location: {name: K}): void
}

interface RouteAct<K, T> {
(location: {name: K, params: T}): void
}

interface Router<T> {
push: T
replace: T
}

interface Routes<T> {
route: Route<T>
done: () => Router<T>
}

interface StaticRouteConfig<K extends string, C> {
name: K
path: StaticPath,
children?: (p: OriginPath) => Routes<C>
component?: {$routes?: {params?: {}}}
}

interface RouteConfig<K extends string, R, P extends R, C>{
name: K
path: Path<P>,
children?: (p: PathMaker<P>) => Routes<C>
component?: {$routes?: {params?: R}}
}

interface Path<T> {
__pathBrand: T
}

interface StaticPath extends Path<{}> {
__staticPathBrand: never
}

interface PathMaker<Parent> {
<A, B>(t: TemplateStringsArray, n: A, n2: B): Path<A & B & Parent>
<A>(t: TemplateStringsArray, n: A): Path<A & Parent>
(t: TemplateStringsArray): Path<Parent>
}

interface OriginPath {
__originPathBrand: never
<A, B>(t: TemplateStringsArray, n: A, n2: B): Path<A & B>
<A>(t: TemplateStringsArray, n: A): Path<A>
(t: TemplateStringsArray): StaticPath
}

var UserComponent = {$routes: {params: { uid: '333'}}}
const unimplement: never = null as never
const route: Route<never> = unimplement
const p: OriginPath = unimplement

const rs =
route({
path: p`user/${{uid: ''}}/profile/${{pageType: ''}}`,
name: 'user',
component: UserComponent,
children: p => route({
name: 'tab',
path: p`tab/${{tab: ''}}`,
component: {$routes: {params: {uid: '123', tab: '333'}}},
})
})
.route({
path: p`post`,
name: 'post'
})
.done()

rs.push({name: 'post'})
dark
sans