I'm working on algebraic data types in ts and stuck with typing the match function. Simplified version of the problem is demonstrated below:
let rec = { a: 'a', b: 1 };
// just captures type of 'rec' and then reuses the type for eval
const makeEval = <Record>(rec: Record) => <
Res,
K extends keyof Record = keyof Record
>(
val: Record,
cases:
| Cases<Record, Res>
| (Cases<Record, Res, K> & { else: (r: Record) => Res })
): Res => (undefined as any) as Res;
export type Cases<Record, Res, K extends keyof Record = keyof Record> = {
[T in K]: (value: Record[T]) => Res
};
const evalMyRecord = makeEval(rec);
// this is fine
const a = evalMyRecord(rec, { a: s => s, b: n => n.toString() });
// err
const b = evalMyRecord(rec, { a: s => s, else: _ => 'why err?' });
// Property 'b' is missing in type '{ a: (s: string) => string; else: (_: { a: string; b: number; }) => string; }'.
// requires explicit subset
const b_ = evalMyRecord<string, 'a'>(rec, {
a: s => s,
else: _ => 'but explicit a is fine'
});
// full set is fine
const c = evalMyRecord(rec, {
a: s => s,
b: n => 'n',
else: _ => 'fine too'
});
So I want to express a type that is either has all keys of Record or any subset of it + {else} case.
I know that there is a solution with Partial:
type EvalCases<Record, Res> =
| Cases<Record, Res>
| (Partial<Cases<Record, Res>> & { else: (r: Record) => Res });
const b = evalMyRecord(rec, { a: s => s, else: _ => 'works' });
const b2 = evalMyRecord(rec, {
a: s => s,
b: undefined,
else: _ => 'also works but weird'
});
But having: {b: undefined} looks a bit off. I want a compiler error if any case is not a proper function (if possible).
Any suggestions on typescript magic?
NOTE: conceptually close (but no the same) to Typescript cannot infer correct argument types for an object of functions.
The following solution allows you to have either Cases<Record, Res>
or any subset of Cases<Record, Res>
plus the else
property.
type EnforcingPartial<T> = {
[key in keyof T]: { [subKey in key]: T[key]; }
}[keyof T];
type EvalCases<Record, Res> = Cases<Record, Res> |
(EnforcingPartial<Cases<Record, Res>> & { else: (r: Record) => Res });
const b = evalMyRecord(rec, { a: s => s, else: _ => 'works' });
const b2 = evalMyRecord(rec, {
a: s => s,
b: undefined, // ERROR HERE, b is incompatible
else: _ => 'also works but weird'
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With