如何使用"path tuple"从嵌套属性中检索类型



给定Typescript中发现的这段(惊人的(代码:嵌套对象的deep keyof

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无法胜任这项任务。

更新TS 4.1

既然TypeScript支持递归条件类型和可变元组类型,那么可以更简单地编写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;

这可能仍然有一些";有趣的";在类树类型上的行为,但自从我写下下面的答案以来,情况肯定有所改善:

游乐场代码链接。


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

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

给定采用类似[1,2,3]的元组类型并移除第一个元素以产生类似[2, 3]:的较小元组的Tail<T>

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

我将把DeepIndex<T, KS, F>定义为接受一个类型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类型的objKS类型的keys,并且应该产生DeepIndex<T, KS>类型的结果。该实现使用CCD_ 20。让我们看看它是否有效:

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<T>的类型,而不是输出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

所以我放弃了。如果你敢的话,可以再做更多的工作。整个努力真的把事情推向了一个我不愿意让任何其他服从的水平;编译器的有趣和令人兴奋的挑战";而不是";任何人的生计都应该依赖的准则";。


好的,希望能有所帮助;祝你好运

游乐场链接到代码

最新更新