TS 4.1 更新
既然 TypeScript 支持recursive conditional types 和variadic 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<T> 采用像 [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<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 类型的obj 和keys 类型KS,并且应该产生DeepIndex<T, KS> 类型的结果。该实现使用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<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
所以我放弃了。如果你敢的话,请随意做更多的工作。整个努力确实将事情推向了一个我不会让任何人其他服从的水平。我认为这是“对编译器来说既有趣又令人兴奋的挑战”,而不是“任何人的生计都应该依赖的代码”。
好的,希望对您有所帮助;祝你好运!
Playground link to code