Skip to content

Instantly share code, notes, and snippets.

@jrf0110
Created August 28, 2019 17:25
Show Gist options
  • Save jrf0110/507a3ef3c5d8e1e913932402bdf1b0e3 to your computer and use it in GitHub Desktop.
Save jrf0110/507a3ef3c5d8e1e913932402bdf1b0e3 to your computer and use it in GitHub Desktop.
/**
* Overwrite certain keys with new types.
*
* @example
* type A = {a: string, b?: string}
* type result = Overwrite<A, {b: string}> = {a: string, b: string}
* (Note that b is no longer optional)
*/
type Overwrite<T1, T2> = { [P in Exclude<keyof T1, keyof T2>]: T1[P] } & T2
function hasProperties<T extends object, K extends string>(
obj: T,
...keys: K[]
): obj is T & { [J in K]: unknown } {
return !!obj && keys.every(key => obj.hasOwnProperty(key))
}
export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head' | 'options'
export interface UrlPatternResult<Param extends string> {
paramNames: Param[]
pattern: string
}
export function $url<Param extends string>(
strings: TemplateStringsArray,
...paramNames: Param[]
): UrlPatternResult<Param> {
return {
paramNames,
pattern: strings.reduce((result, str, i) => `${result}${str}${paramNames[i] || ''}`, ''),
}
}
class EntityParseError extends Error {}
export function makeFetcher<
FetchMethod extends Method,
Param extends string,
Entity extends { id: string }
>(
method: FetchMethod,
patternResult: UrlPatternResult<Param>,
parseEntity: (input: unknown) => Entity | null,
): FetchMethod extends 'get'
? (args: { [K in Param]: string }) => Promise<Entity>
: FetchMethod extends 'delete' | 'options'
? (args: { [K in Param]: string }) => Promise<void>
: FetchMethod extends 'patch'
? (args: { [K in Param]: string }, payload: Partial<Entity>) => Promise<Entity>
: FetchMethod extends 'post' | 'put'
? (args: { [K in Param]: string }, payload: Overwrite<Entity, { id?: string }>) => Promise<Entity>
: never {
const fetcher = async (
args: { [K in Param]: string },
payload?: Partial<Entity> | Entity,
requestInit?: Partial<RequestInit>,
) => {
const url = patternResult.paramNames.reduce(
(result, param) => result.replace(new RegExp(`/${param}/`, 'g'), args[param]),
patternResult.pattern,
)
const res = await fetch(url, {
method: method.toUpperCase(),
...(payload && { body: JSON.stringify(payload) }),
...requestInit,
headers: {
'Content-Type': 'application/json',
...(requestInit && requestInit.headers),
},
})
const entity = parseEntity(await res.json())
if (entity === null) {
throw new EntityParseError('Invalid response body')
}
return entity
}
// TODO: handle all da' cases
return fetcher as any
}
import { makeFetcher } from './makeFetcher'
interface Book {
id: string
name: string
numPages: number
}
function parseBook(input: unknown): Book | null {
if (typeof input === 'object' && input && hasProperties(input, 'id', 'name', 'numPages')) {
const { id, name, numPages } = input
if (typeof id === 'string' && typeof name === 'string' && typeof numPages === 'number') {
return { id, name, numPages }
}
}
return null
}
const fetchBook = makeFetcher('get', $url`/accounts/${'accountId'}/books/${'bookId'}`, parseBook)
fetchBook({
accountId: '123',
bookId: 'abc',
})
.then(book => {
console.log(book.id)
})
.catch(e => {
if (e instanceof EntityParseError) {
} else {
}
})
const createBook = makeFetcher('post', $url`/accounts/${'accountId'}/domains`, parseBook)
createBook(
{ accountId: '123' },
{
name: 'Adventures of foo and bar',
numPages: 123,
},
).then(book => {})
interface Beer {
id: string
name: string
ounces: number
}
function parseBeer(input: unknown): Beer | null {
return null
}
const fetchBeer = makeFetcher('get', $url`/api/accounts/${'accountId'}/beer/${'beerId'}`, parseBeer)
fetchBeer({
accountId: '123',
beerId: 'shiner-bock',
})
.then(beer => {
beer
})
.catch(e => {
if (e instanceof EntityParseError) {
//
} else {
}
})
const createBeer = makeFetcher('post', $url`/api/accounts/${'accountId'}/beer`, parseBeer)
createBeer(
{
accountId: '123',
},
{
name: 'Shiner Bock',
ounces: 16,
},
).then(beer => {
beer.id
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment