【问题标题】:How to retrieve a type from a nested property using a "path tuple"如何使用“路径元组”从嵌套属性中检索类型
【发布时间】:2020-08-21 22:31:18
【问题描述】:

鉴于在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]
    : [];

这有助于我们将对象的嵌套路径作为元组的并集,如下所示:

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']

我正在寻找使用路径元组从嵌套属性中检索类型的“反向”方法,格式为:

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

问题是编译器对这个签名不满意:Type instantiation is excessively deep and possibly infinite

我找到了一种通过缩小T 来消除此错误的方法:

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

但它只适用于根级别的路径,我担心我的 typescript-foo 无法胜任这项任务。

【问题讨论】:

    标签: typescript


    【解决方案1】:

    TS 4.1 更新

    既然 TypeScript 支持recursive conditional typesvariadic tuple types,你可以更简单地写DeepIndex

    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;
    

    这可能仍然对树状类型有一些“有趣”的行为,但是自从我在下面写下答案后,情况肯定有所改善:

    Playground link to code.


    因此,当我尝试使用与链接问题中相同的不受支持的递归来编写类似的深度索引类型时,我也不断遇到编译器警告或减速。这只是推动编译器做它不应该做的事情的问题之一。也许有一天会出现一种安全、简单且受支持的解决方案,但现在还没有。有关获得对循环条件类型的支持的讨论,请参阅 microsoft/TypeScript#26980

    现在我要做的是编写递归条件类型的旧备用:获取预期的递归类型并将其展开为一系列非递归类型,这些类型在一定深度显式退出:

    假设 Tail&lt;T&gt; 采用像 [1,2,3] 这样的元组类型并删除第一个元素以生成像 [2, 3] 这样的更小的元组:

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

    我将 DeepIndex&lt;T, KS, F&gt; 定义为接受类型 T 和键类型元组 KS 并使用这些键向下进入 T 的东西,生成在那里找到的嵌套属性的类型.如果这最终试图用它没有的键索引到某个东西,它将产生一个失败类型F,它应该默认为类似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;
    

    在这里您可以看到Idx 类型是如何几乎递归的,但它不是引用自身,而是引用另一个几乎相同的类型,最终摆脱了 10 层深度.


    我想像这样使用它:

    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);
    }
    

    因此您可以看到deepIndex() 采用T 类型的objkeys 类型KS,并且应该产生DeepIndex&lt;T, KS&gt; 类型的结果。该实现使用keys.reduce()。让我们看看它是否有效:

    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
    

    我觉得不错。


    请注意,我确定您希望 deepIndex() 函数或 DeepIndex 类型实际上 约束 KS 类型是来自 Paths&lt;T&gt; 的类型,而不是输出undefined。我尝试了大约五种不同的方法来做到这一点,其中大多数完全破坏了编译器。那些没有让编译器崩溃的比上面的更丑更复杂,而且对于一个踢球者来说,他们真的没有给出有用的错误信息;我不久前提交了一个问题microsoft/TypeScript#28505 的一个错误导致错误出现在keys 数组的错误元素上。所以你会想看看

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

    但实际上会发生的是

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

    所以我放弃了。如果你敢的话,请随意做更多的工作。整个努力确实将事情推向了一个我不会让任何人其他服从的水平。我认为这是“对编译器来说既有趣又令人兴奋的挑战”,而不是“任何人的生计都应该依赖的代码”。


    好的,希望对您有所帮助;祝你好运!

    Playground link to code

    【讨论】:

    • 感谢您抽出宝贵时间以如此详细的方式回答。您对 TypeScript 编译器的见解非常有价值且平易近人,您可以在博客中介绍它。
    猜你喜欢
    • 2010-12-30
    • 1970-01-01
    • 2012-06-03
    • 2011-07-26
    • 1970-01-01
    • 2012-06-13
    • 1970-01-01
    • 2021-11-09
    • 2012-08-19
    相关资源
    最近更新 更多