Created
December 1, 2019 23:10
-
-
Save reverofevil/6b3ad32b1f07177de262d84a4a87cd49 to your computer and use it in GitHub Desktop.
io-ts done right: arbitrarily handled runtime type information (overloading) for typescript
This file contains 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
// hkt.ts | |
interface TagToHkt<T> { } | |
export type Tag = keyof TagToHkt<any> | |
export type Apply<F extends Tag, T> = TagToHkt<T>[F] | |
// util.ts | |
export type Intersect<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) | |
? I | |
: never; | |
export type IndexForSure<O, K extends string> = O extends { [K1 in K]: any } ? O[K] : never | |
export type IntersectValues<O> = IndexForSure< | |
Intersect<{ [K in keyof O]: { foo: O[K] } }[keyof O]>, | |
"foo" | |
> | |
type Json = string | number | boolean | null | Array<Json> | { [K: string]: Json } | |
function die(cond: boolean, context: string[], message: string): asserts cond { | |
throw new Error(`At ${context.join('.')}: ${message}`); | |
} | |
// rtti.ts | |
/** | |
Interface of interpreter of runtime type information | |
@param F type of container of interpreter's result | |
*/ | |
export interface RttiInterpreter<F extends Tag> { | |
string: Apply<F, string>; | |
number: Apply<F, number>; | |
boolean: Apply<F, boolean>; | |
null: Apply<F, null>; | |
literal: <T extends string | number | boolean | null>(t: T) => Apply<F, T>; | |
array: <T>(p: Apply<F, T>) => Apply<F, T[]>; | |
object: <O>(ps: {[K in keyof O]: Apply<F, O[K]>}) => Apply<F, O>; | |
maybe: <T>(p: Apply<F, T>) => Apply<F, T | undefined>; | |
union: <T>(...ps: T[]) => Apply<F, T>; | |
intersection: <O>(ps: { [K in keyof O]: Apply<F, O[K]> }) => Apply<F, IntersectValues<O>>; | |
disjointUnion: <K extends string, O extends Record<any, object>>( | |
key: K, | |
ps: {[L in keyof O]: Apply<F, O[L]>}, | |
) => Apply<F, { | |
[L in keyof O]: { [K1 in K]: L } & O[L] | |
}[keyof O]>; | |
} | |
// parser.ts | |
export type Parser = 'Parser' | |
/* declare module './hkt.ts' { */ | |
interface TagToHkt<T> { | |
Parser: (json: Json | undefined, context: string[]) => T; | |
} | |
/* } */ | |
export const parseAlgebra: RttiInterpreter<Parser> = { | |
literal: x => (json, context) => { | |
die( | |
x !== json, | |
context, | |
`Wrong value: '${json}' instead of '${x}'`, | |
); | |
return x; | |
}, | |
// ... | |
}; | |
export const parse = <R>( | |
type: (alg: RttiInterpreter<Parser>) => Apply<Parser, R>, | |
json: Json, | |
): R => { | |
return type(parseAlgebra)(json, []); | |
}; | |
// example-types.ts | |
export const Foo = <F extends Tag>(t: RttiInterpreter<F>) => t.object({ | |
id: t.string, | |
type: t.string, | |
text: t.string, | |
}); | |
export type Foo = ReturnType<ReturnType<typeof Foo>>; | |
export const Bar = <F extends Tag>(t: RttiInterpreter<F>) => t.intersection({ | |
main: t.object({ | |
id: t.string, | |
type: t.string, | |
text: t.string, | |
}), | |
union: t.disjointUnion('type', { | |
'this': t.object({ | |
foo: t.array(Foo(t)), | |
}), | |
'that': t.object({ | |
bar: t.array(Foo(t)), | |
}), | |
}), | |
}); | |
export type Bar = ReturnType<ReturnType<typeof Bar>>; | |
// example.ts | |
const result = parse(Foo, {}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment