Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
806 views
in Technique[技术] by (71.8m points)

typescript - Inferring nested value types with consideration for intermediate optional keys

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

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...