Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

typescript cannot infer subset of keys of object from usage

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.

like image 839
Simon Korzunov Avatar asked Nov 08 '22 08:11

Simon Korzunov


1 Answers

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'
});
like image 77
Behrooz Avatar answered Nov 14 '22 23:11

Behrooz