Last active
June 27, 2018 02:23
-
-
Save OliverJAsh/538c46f682c40054a68e13e00b3ff9d6 to your computer and use it in GitHub Desktop.
Type safe routing with `fp-ts-routing` and `unionize`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
end, | |
int, | |
lit, | |
Match, | |
parse, | |
Route as RouteBase, | |
zero | |
} from "fp-ts-routing"; | |
import { pipe } from "fp-ts/lib/function"; | |
import { ofType, unionize, UnionOf } from "unionize"; | |
// | |
// `Route`s | |
// This defines the structured data of our routes | |
// | |
const Route = unionize({ | |
Home: {}, | |
User: ofType<{ userId: number }>(), | |
Invoice: ofType<{ userId: number; invoiceId: number }>(), | |
NotFound: {} | |
}); | |
type Route = UnionOf<typeof Route>; | |
// | |
// `Match`s | |
// This defines how to match a string representing the URL path | |
// | |
const home = end; | |
const _user = lit("users").then(int("userId")); | |
const user = _user.then(end); | |
const invoice = _user | |
.then(lit("invoice")) | |
.then(int("invoiceId")) | |
.then(end); | |
// | |
// Router | |
// This ties our `Match`s together with our `Route`s. We define the order to run the `Match`s, and | |
// how to parse a `Match` (string) into a `Route` (object). | |
// | |
const router = zero<Route>() | |
.alt(home.parser.map(() => Route.Home({}))) | |
.alt(user.parser.map(({ userId }) => Route.User({ userId }))) | |
.alt( | |
invoice.parser.map(({ userId, invoiceId }) => | |
Route.Invoice({ userId, invoiceId }) | |
) | |
); | |
// | |
// Helpers | |
// | |
const parseRoute = (s: string): Route => | |
parse(router, RouteBase.parse(s), Route.NotFound({})); | |
const formatRoute = <A extends object>(match: Match<A>) => (route: A): string => | |
match.formatter.run(RouteBase.empty, route).toString(); | |
// | |
// Parsers example | |
// | |
console.log("Parsers"); | |
console.log(parseRoute("/invalid-route")); // => Route.NotFound({}) | |
console.log(parseRoute("/")); // => Route.Home({}) | |
console.log(parseRoute("/users/1")); // => Route.User({ userId: 1 }) | |
console.log(parseRoute("/users/foo")); // => Route.NotFound({}) | |
console.log(parseRoute("/users/1/invoice/2")); // => Route.Invoice({ userId: 1, invoiceId: 2 }) | |
// | |
// Route matchers | |
// After we've parsed a route, we can match against it. | |
// | |
const matchRoute = Route.match({ | |
Home: () => "Welcome!", | |
User: ({ userId }) => `User ID ${userId}`, | |
Invoice: ({ userId, invoiceId }) => | |
`User ID ${userId}, invoice ID ${invoiceId}`, | |
NotFound: () => "Not found" | |
}); | |
const matchRouteString = pipe( | |
parseRoute, | |
matchRoute | |
); | |
// | |
// Route matcher example | |
// | |
console.log("Route matcher example"); | |
console.log(matchRouteString("/invalid-route")); // => "Not found" | |
console.log(matchRouteString("/")); // => "Welcome!" | |
console.log(matchRouteString("/users/1")); // => "User ID 1" | |
console.log(matchRouteString("/users/foo")); // => "Not found" | |
console.log(matchRouteString("/users/1/invoice/2")); // => "User ID 1, invoice ID 2" | |
// | |
// Formatters example | |
// | |
console.log("Formatters example"); | |
console.log(formatRoute(home)({})); // => "/" | |
console.log(formatRoute(user)({})); // => Type error! Property 'userId' is missing in type '{}'. | |
console.log(formatRoute(user)({ userId: "foo" })); // => Type error! Type 'string' is not assignable to type 'number'. | |
console.log(formatRoute(user)({ userId: 1 })); // => "/users/1" | |
console.log(formatRoute(invoice)({ userId: 1, invoiceId: 2 })); // => Type error! |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment