Last active
April 17, 2019 01:07
-
-
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.
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
//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); |
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
//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") | |
); |
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
//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