问题
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!
Playground link
回答1:
There might be other ways of accomplishing what you're trying to do, but the immediate problem that you're facing is you are accidentally distributing your conditional type in the definition of OptVal
. Since PRED
is a type parameter, the conditional check PRED extends never ? RES : RES | undefined
will end up splitting PRED
into its union members, evaluating the conditional for each member, and unioning back together for the result. And your problem case is when PRED
is never
. You might not think of never
as being a union type, but for consistency's sake the compiler considers it to be the "empty union" and the output will also be an empty union, aka never
.
The easiest way to turn off distributive conditional types is to take the naked type parameter PRED
and "clothe" it in a single-element tuple type like this:
type OptVal<PRED, RES> = [PRED] extends [never] ? RES : RES | undefined;
And this will make your cases work as desired, I think:
type A3 = OptVal1<Foo, "a">; // { b?: number | undefined; }
Okay, hope that helps; good luck!
Playground link to code
来源:https://stackoverflow.com/questions/60869412/inferring-nested-value-types-with-consideration-for-intermediate-optional-keys