Inferring nested value types with consideration for intermediate optional keys
Asked Answered
L

1

3

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

Lyndseylyndsie answered 26/3, 2020 at 14:22 Comment(0)
G
5

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" (see this comment on ms/TS#23182) 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; }

Playground link to code

Galluses answered 26/3, 2020 at 15:19 Comment(1)
This makes perfect sense & thank you for reminding me of and explaining this particular aspect of conditional type handling! Works great now!Lyndseylyndsie

© 2022 - 2024 — McMap. All rights reserved.