Inferring nested value types with consideration for intermediate optional keys

痞子三分冷 提交于 2021-02-02 03:37:42

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!