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
1.2k views
in Technique[技术] by (71.8m points)

typescript - How to retrieve a type from a nested property using a "path tuple"

Given this (amazing) piece of code found in Typescript: deep keyof of a nested object

type Cons<H, T> = T extends readonly any[] ?
    ((h: H, ...t: T) => void) extends ((...r: infer R) => void) ? R : never
    : never;

type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]]

type Paths<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
    { [K in keyof T]-?: [K] | (Paths<T[K], Prev[D]> extends infer P ?
        P extends [] ? never : Cons<K, P> : never
    ) }[keyof T]
    : [];

which helps us get the nested paths of an object as an union of tuples, like so:

type Obj = {
  A: { a1: string }
  B: { b1: string, b2: { b2a: string } }
}

type ObjPaths = Paths<obj> // ['A'] | ['A', 'a1'] | ['B'] | ['B', 'b1'] | ['B', 'b2'] | ['B', 'b2', 'b2a']

I'm looking for the "reverse" way to retrieve a type from a nested property using a path tuple, in the form of :

type TypeAtPath<T extends object, U extends Paths<T>> = ...

The problem is that the compiler is not happy with this signature: Type instantiation is excessively deep and possibly infinite.

I found a way to remove this error by narrowing T:

type TypeAtPath<T extends {[key: string]: any}, U extends Paths<T>> = T[U[0]]

But it only works for paths at the root level, and I'm affraid my typescript-foo is not up to this task.

See Question&Answers more detail:os

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

1 Reply

0 votes
by (71.8m points)

UPDATE FOR TS 4.1

Now that TypeScript supports recursive conditional types and variadic tuple types, you can write DeepIndex more simply:

type DeepIndex<T, KS extends Keys, Fail = undefined> =
    KS extends [infer F, ...infer R] ? F extends keyof T ? R extends Keys ?
    DeepIndex<T[F], R, Fail> : Fail : Fail : T;

This still probably has some "interesting" behavior on tree-like types, but the situation has definitely improved since I wrote the answer below:

Playground link to code.


So, when I tried to write a similar deep-indexing type using the same kind of unsupported recursion as in the linked question, I also kept running into either compiler warnings or slowdowns. This is just one of the problems with pushing the compiler to do things it isn't meant to do. Maybe one day there will be a safe, simple, and supported solution, but right now there isn't. See microsoft/TypeScript#26980 for discussion about getting support for circular conditional types.

For now what I'm going to do is my old standby for writing recursive conditional types: take the intended recursive type and unroll it into a series of non-recursive types that explicitly bails out at some depth:

Given Tail<T> that takes a tuple type like [1,2,3] and removes the first element to produce a smaller tuple like [2, 3]:

type Tail<T> = T extends readonly any[] ?
    ((...t: T) => void) extends ((h: any, ...r: infer R) => void) ? R : never
    : never;

I'll define DeepIndex<T, KS, F> to be something that takes a type T and a tuple of key-types KS and walks down into T with those keys, producing the type of the nested property found there. If this ends up trying to index into something with a key it doesn't have, it will produce a failure type F, which should default to something like undefined:

type Keys = readonly PropertyKey[];
type DeepIndex<T, KS extends Keys, F = undefined> = Idx0<T, KS, F>;
type Idx0<T, KS extends Keys, F> = KS['length'] extends 0 ? T : KS[0] extends keyof T ? Idx1<T[KS[0]], Tail<KS>, F> : F;
type Idx1<T, KS extends Keys, F> = KS['length'] extends 0 ? T : KS[0] extends keyof T ? Idx2<T[KS[0]], Tail<KS>, F> : F;
type Idx2<T, KS extends Keys, F> = KS['length'] extends 0 ? T : KS[0] extends keyof T ? Idx3<T[KS[0]], Tail<KS>, F> : F;
type Idx3<T, KS extends Keys, F> = KS['length'] extends 0 ? T : KS[0] extends keyof T ? Idx4<T[KS[0]], Tail<KS>, F> : F;
type Idx4<T, KS extends Keys, F> = KS['length'] extends 0 ? T : KS[0] extends keyof T ? Idx5<T[KS[0]], Tail<KS>, F> : F;
type Idx5<T, KS extends Keys, F> = KS['length'] extends 0 ? T : KS[0] extends keyof T ? Idx6<T[KS[0]], Tail<KS>, F> : F;
type Idx6<T, KS extends Keys, F> = KS['length'] extends 0 ? T : KS[0] extends keyof T ? Idx7<T[KS[0]], Tail<KS>, F> : F;
type Idx7<T, KS extends Keys, F> = KS['length'] extends 0 ? T : KS[0] extends keyof T ? Idx8<T[KS[0]], Tail<KS>, F> : F;
type Idx8<T, KS extends Keys, F> = KS['length'] extends 0 ? T : KS[0] extends keyof T ? Idx9<T[KS[0]], Tail<KS>, F> : F;
type Idx9<T, KS extends Keys, F> = KS['length'] extends 0 ? T : KS[0] extends keyof T ? IdxX<T[KS[0]], Tail<KS>, F> : F;
type IdxX<T, KS extends Keys, F> = KS['length'] extends 0 ? T : KS[0] extends keyof T ? T[KS[0]] : F;

Here you can see how the Idx type is nearly recursive, but instead of referencing itself, it references another nearly identical type, eventually bailing out 10 levels deep.


I'd imagine using it like this:

function deepIndex<T, KS extends Keys, K extends PropertyKey>(
  obj: T, 
  ...keys: KS & K[]
): DeepIndex<T, KS>;
function deepIndex(obj: any, ...keys: Keys) {
    return keys.reduce((o, k) => o?.[k], obj);
}

So you can see that deepIndex() takes obj of type T and keys of type KS, and should produce a result of type DeepIndex<T, KS>. The implementation uses keys.reduce(). Let's see if it works:

const obj = {
    a: { b: { c: 1 }, d: { e: "" } },
    f: { g: { h: { i: true } } }, j: { k: [{ l: "hey" }] }
}

const c = deepIndex(obj, "a", "b", "c"); // number 
const e = deepIndex(obj, "a", "d", "e"); // string
const i = deepIndex(obj, "f", "g", "h", "i"); // boolean
const l = deepIndex(obj, "j", "k", 0, "l"); // string
const oops = deepIndex(obj, "a", "b", "c", "d"); // undefined
const hmm = deepIndex(obj, "a", "b", "c", "toFixed"); // (fractionDigits?: number) => string

Looks good to me.


Note that I'm sure you'd love to have the deepIndex() function or the DeepIndex type actually constrain the KS type to be those from Paths<T> instead of outputting undefined. I tried about five different ways to do this, and most of them blew out the compiler entirely. And the ones that didn't blow out the compiler were uglier and more complicated than the above, and for a kicker they really didn't give useful error messages; a bug I filed an issue about a while ago, microsoft/TypeScript#28505, causes the error to appear on the wrong element of the keys array. So you'd want to see

const oops = deepIndex(obj, "a", "b", "c", "d"); // error!
// --------------------------------------> ~~~
// "d" is not assignable to keyof number

but what would actually happen is

const oops = deepIndex(obj, "a", "b", "c", "d"); // error!
// -----------------------> ~~~
// "d" is not assignable to never

So I give up. Feel free to work on that more, if you dare. The whole endeavor really pushes things to a level I wouldn't feel comfortable subjecting anyone else to. I see this as "fun and exciting challenge for the compiler" and not "code on which anyone's livelihood should depend".


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

...