Created
October 20, 2022 16:59
-
-
Save sanisoclem/518c73c0b058a4ec48bcaf8acdc52abc to your computer and use it in GitHub Desktop.
TypeScript HKTs - Codec, Forms
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
/// Roughly | |
///type TxnSchema :: (Type -> Type) -> Row Type | |
///type TxnSchema f = ( | |
/// name :: f String | |
/// amount :: f Number | |
///); | |
/// | |
///type Codec a = | |
/// { encode: a -> Json | |
/// , decode: Json -> Option a } | |
/// | |
///type TxnCodec = TxnSchema Codec | |
///type Txn = {| TxnSchema } | |
export interface Kind<F, A> { | |
readonly _F: F; | |
readonly _A: A; | |
// feels like a hack | |
// but seems like I really need Kind<F,A> -> ResolveHKT<F,A> | |
// because there is no generic derive | |
readonly cons: () => ResolveHKT<F, A>; | |
} | |
export interface HKT { | |
readonly _F: unknown; | |
} | |
export interface Schemer<F> { | |
readonly string: Kind<F, string>; | |
readonly int: Kind<F, number>; | |
readonly struct: <A>(def: { [K in keyof A]: Kind<F, A[K]> }) => Kind<F, { [K in keyof A]: A[K] }>; | |
} | |
export type TypeOf<S> = S extends Schema<infer A> ? A : never; | |
export type ResolveHKT<F, A> = F extends HKT ? Omit<{ _F: A } & F, '_F'> : never; | |
export interface Schema<A> { | |
<F>(T: Schemer<F>): Kind<F, A>; | |
} | |
export interface CodecHKT extends HKT { | |
encode: (x: this['_F']) => unknown; | |
decode: (x: unknown) => this['_F'] | null; | |
} | |
export class CodecSchemer implements Schemer<CodecHKT> { | |
readonly string: Kind<CodecHKT, string> = { | |
_F: null as any, | |
_A: null as any, | |
cons: () => ({ | |
encode: (x: string) => x as unknown, | |
decode: (x: unknown) => x as string | |
}) | |
}; | |
readonly int: Kind<CodecHKT, number> = { | |
_F: null as any, | |
_A: null as any, | |
cons: () => ({ | |
encode: (x: number) => x as unknown, | |
decode: (x: unknown) => { | |
const r = parseInt(`${x}`); | |
if (isNaN(r)) return null; | |
return r; | |
} | |
}) | |
}; | |
readonly struct = <A>(def: { [K in keyof A]: Kind<CodecHKT, A[K]> }): Kind< | |
CodecHKT, | |
{ [K in keyof A]: A[K] } | |
> => { | |
return { | |
_F: null as any, | |
_A: null as any, | |
cons: () => ({ | |
encode: (x: A) => { | |
const keys: (keyof A)[] = Object.keys(def) as (keyof A)[]; | |
return keys.reduce((acc, key) => ({ ...acc, [key]: def[key].cons().encode(x[key]) }), {}); | |
}, | |
decode: (x: unknown): A | null => { | |
const keys: (keyof A)[] = Object.keys(def) as (keyof A)[]; | |
return keys.reduce((acc, key) => { | |
if (acc === null) return null; | |
const v = def[key].cons().decode((x as any)[key]); | |
// ambiguous, use option type | |
if (v === null) return null; | |
return { ...acc, [key]: v }; | |
}, {} as A | null); | |
} | |
}) | |
}; | |
}; | |
resolve<A>(f: Kind<CodecHKT, A>): ResolveHKT<CodecHKT, A> { | |
return f.cons(); | |
} | |
} | |
export const define = <A>(s: Schema<A>): Schema<A> => s; | |
export const createCodec = <A>(fn: Schema<A>): ResolveHKT<CodecHKT, A> => { | |
const codecSchemer = new CodecSchemer(); | |
const codec = fn(codecSchemer); | |
return codecSchemer.resolve(codec); | |
}; | |
// Tests | |
const txnSchema = define((F) => | |
F.struct({ | |
name: F.string, | |
date: F.int | |
}) | |
); | |
const txnCodec = createCodec(txnSchema); | |
type Txn = TypeOf<typeof txnSchema>; | |
const value: Txn = { | |
name: '', | |
date: 0 | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment