I'm trying to define helper types for determining the type of nested object values, whilst also considering any optional parent keys, e.g. in structures like these (or deeper):
type Foo = { a: { b?: number; } };
type Foo2 = { a?: { b: number } };
For my purposes, the type of b
in both Foo
and Foo2
should be inferred as number | undefined
. In Foo2
the b
is not optional itself, but because a
is, for my lookup purposes b
must now be optional too... so much for context.
Using these helper types (extracted from a larger set) as building blocks:
type Keys<T> = keyof Required<T>;
type IsOpt<T> = T extends undefined ? true : never;
type HasOptKey1<T, A> = A extends Keys<T> ? IsOpt<T[A]> : never;
type HasOptKey2<T, A, B> = A extends Keys<T>
? IsOpt<T[A]> extends never
? HasOptKey1<T[A], B>
: true
: never;
type Val1<T, A> = A extends Keys<T> ? T[A] : never;
type Val2<T, A, B> = A extends Keys<T> ? Val1<Required<T>[A], B> : never;
Putting these to good use, we get:
type F1 = HasOptKey1<Foo, "a">; // never - CORRECT!
type F2 = HasOptKey1<Foo2, "a">; // true - CORRECT!
type F3 = HasOptKey2<Foo, "a", "b">; // true - CORRECT!
type F4 = HasOptKey2<Foo2, "a", "b">; // true - CORRECT!
// infer type of `a` in Foo
type A1 = HasOptKey1<Foo, "a"> extends never
? Val1<Foo, "a">
: Val1<Foo, "a"> | undefined;
// { b: number | undefined; } - CORRECT!
// infer type of `a` in Foo2
type A2 = HasOptKey1<Foo2, "a"> extends never
? Val1<Foo2, "a">
: Val1<Foo2, "a"> | undefined;
// { b: number } | undefined - CORRECT!
// infer type of `b` in Foo
type B1 = HasOptKey2<Foo, "a", "b"> extends never
? Val2<Foo, "a", "b">
: Val2<Foo, "a", "b"> | undefined;
// number | undefined - CORRECT!
// infer type of `b` in Foo2
type B2 = HasOptKey2<Foo2, "a", "b"> extends never
? Val2<Foo2, "a", "b">
: Val2<Foo2, "a", "b"> | undefined;
// number | undefined - CORRECT!
To avoid these repeated conditionals, I wanted to use another helper type:
// helper type w/ same logic as used for A1/A2/B1/B2 conditionals
type OptVal<PRED, RES> = PRED extends never ? RES : RES | undefined;
// applied
type OptVal1<T, A> = OptVal<HasOptKey1<T, A>, Val1<T, A>>;
type OptVal2<T, A, B> = OptVal<HasOptKey2<T, A, B>, Val2<T, A, B>>;
However, even though it seems to be working for 3 out of 4 cases, A3
is incorrectly inferred as never
and I don't understand why:
type A3 = OptVal1<Foo, "a">;
// never - WHHHYYY??? (should be same as A1!) <-----
type A4 = OptVal1<Foo2, "a">;
// { b: number } | undefined - CORRECT! (same as A2)
type B3 = OptVal2<Foo, "a", "b">; // number | undefined - CORRECT!
type B4 = OptVal2<Foo2, "a","b">; // number | undefined - CORRECT!