Skip to content

Instantly share code, notes, and snippets.

@sanisoclem
Created October 20, 2022 16:59
Show Gist options
  • Save sanisoclem/518c73c0b058a4ec48bcaf8acdc52abc to your computer and use it in GitHub Desktop.
Save sanisoclem/518c73c0b058a4ec48bcaf8acdc52abc to your computer and use it in GitHub Desktop.
TypeScript HKTs - Codec, Forms
/// 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