Skip to content

Instantly share code, notes, and snippets.

@reidev275
Last active April 17, 2019 01:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save reidev275/3c894c54631b926e37223f76bd137951 to your computer and use it in GitHub Desktop.
Save reidev275/3c894c54631b926e37223f76bd137951 to your computer and use it in GitHub Desktop.
Type safe api and client. By defining an interface, some mapped types, and some generic helpers we're able to interpret the interface into a type safe server and client library.
//code that exists in the server project
import { AsEndpoint, Handler, generateClient } from "./endpoint"
import * as express from "express";
//Our Api for communication between server and client
export interface Api {
getPokemonByName(name: string): Pokemon | undefined;
allPokemon(): Pokemon[];
}
//The Api type mapped to an Endpoint<Api> type
//each property must match a property from the Api interface
//the path property for getPokemonByName must match the argument type from the Api type
export const api: AsEndpoint<Api> = {
allPokemon: {
method: "get",
path: () => "/api/pokemon",
route: "/api/pokemon"
},
getPokemonByName: {
method: "get",
path: (name: string) => "/api/pokemon/" + name,
route: "/api/pokemon/:name"
}
};
//mocked for now, but this is another mapped type ensuring type safety
//with our Api type. All inputs for these will be express.Request values
//because of our first type parameter to Handler
export const handler: Handler<express.Request, Api> = {
allPokemon: () => Promise.resolve([]),
getPokemonByName: (req: express.Request) => Promise.resolve(undefined)
};
const app = express();
generateApi(app, api, handler);
//code that exists in the client project
import { fromEndpoint } from "endpoint";
import { api } from "api";
//turning an Api method into a type safe function
export const getPokemonByName = fromEndpoint(api.getPokemonByName);
//example usage
//the promise's type parameter resolves to the type defined in our Api interface
//in this case getPokemonByName returns Pokemon | undefined so we have to validate
//the p is not undefined before we're able to access the .name property
getPokemonByName("Charizard").then(p =>
p ? console.log(p.name) : console.log("not found")
);
//Endpoint type with two type parameters
//I is for the input necessary for the Endpoint
//T is a phantom type used to ensure type safety throughout
export interface Endpoint<I, T> {
method: "get" | "post" | "put" | "delete";
path: (i: I) => string;
route: string;
}
//mapped type that maps all properties of T into various Endpoint types
//depending upon inputs and outputs of the methods on T
export type AsEndpoint<T> = {
[P in keyof T]:
T[P] extends () => any ? Endpoint<void, ReturnType<T[P]>> :
T[P] extends (x: infer X) => any ? Endpoint<X, ReturnType<T[P]>> :
T[P] extends (x: infer X, y: infer Y) => any ? Endpoint<{x: X, y: Y}, ReturnType<T[P]>> :
T[P] extends (...args: any[]) => any ? Endpoint<any[], ReturnType<T[P]>> :
never
};
//mapped type that will map all properties of T to a promise of the return type of the property
//The I type parameter is used to define the input to the handler.
export type Handler<I, T> = {
[P in keyof T]:
T[P] extends (...args: any[]) => any ? (req: I) => Promise<ReturnType<T[P]>> :
never
};
import { Application, Request } from "express";
//generate a typesafe express app per our Api
//current implementation is simplistic
//actual implementation will handle logging, exceptions, etc
export const generateApi = <T>(
app: Application,
endpoint: AsEndpoint<T>,
handler: Handler<Request, T>
): void => {
const inputs = Object.keys(endpoint) as (keyof AsEndpoint<T>)[];
inputs.forEach(k => {
const e = endpoint[k];
const h = handler[k];
app[e.method](e.route, (req, res) => h(req).then(x => res.json(x)));
});
};
import { default as axios } from "axios";
//Turn an endpoint into a typesafe, lazy promise for client side use
export const fromEndpoint = <I, T>(endpoint: Endpoint<I, T>) => (
i: I
): Promise<T> =>
axios({
method: endpoint.method,
url: endpoint.path(i)
}).then(x => x.data as T);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment