Skip to content

Instantly share code, notes, and snippets.

@OliverJAsh
Last active June 27, 2018 02:23
Show Gist options
  • Save OliverJAsh/538c46f682c40054a68e13e00b3ff9d6 to your computer and use it in GitHub Desktop.
Save OliverJAsh/538c46f682c40054a68e13e00b3ff9d6 to your computer and use it in GitHub Desktop.
Type safe routing with `fp-ts-routing` and `unionize`
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