鉴于 Typescript 中发现的这段(令人惊奇的)代码 :嵌套对象的深层 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]
: [];
Run Code Online (Sandbox Code Playgroud)
这有助于我们将对象的嵌套路径作为元组的并集,如下所示:
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']
Run Code Online (Sandbox Code Playgroud)
我正在寻找“反向”方法来使用路径 tuple从嵌套属性中检索类型,其形式为:
type TypeAtPath<T extends object, U extends Paths<T>> = ...
Run Code Online (Sandbox Code Playgroud)
问题是编译器对这个签名不满意:Type instantiation is excessively deep and possibly infinite。
我找到了一种通过缩小范围来消除此错误的方法T:
type TypeAtPath<T extends {[key: string]: any}, U extends Paths<T>> = T[U[0]]
Run Code Online (Sandbox Code Playgroud)
但它只适用于根级别的路径,我担心我的 typescript-foo 无法胜任这项任务。
现在 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;
Run Code Online (Sandbox Code Playgroud)
这在树状类型上可能仍然有一些“有趣”的行为,但自从我写下答案以来,情况肯定有所改善:
因此,当我尝试使用与链接问题中相同类型的不受支持的递归来编写类似的深度索引类型时,我也不断遇到编译器警告或速度减慢的问题。这只是迫使编译器做它不该做的事情的问题之一。也许有一天会出现一种安全、简单且受支持的解决方案,但现在还没有。有关获取循环条件类型支持的讨论,请参阅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;
Run Code Online (Sandbox Code Playgroud)
我将定义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;
Run Code Online (Sandbox Code Playgroud)
在这里,您可以看到该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);
}
Run Code Online (Sandbox Code Playgroud)
所以你可以看到,deepIndex()需要objtypeT和keysof type KS,并且应该产生 type 的结果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
Run Code Online (Sandbox Code Playgroud)
在我看来很好。
请注意,我确信您希望函数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
Run Code Online (Sandbox Code Playgroud)
但实际发生的是
const oops = deepIndex(obj, "a", "b", "c", "d"); // error!
// -----------------------> ~~~
// "d" is not assignable to never
Run Code Online (Sandbox Code Playgroud)
所以我放弃了。如果你敢的话,请随意做更多的工作。整个努力确实将事情推向了一个让其他人都不敢接受的水平。我认为这是“对编译器来说有趣且令人兴奋的挑战”,而不是“任何人的生计都应该依赖的代码”。
好的,希望有帮助;祝你好运!
| 归档时间: |
|
| 查看次数: |
1105 次 |
| 最近记录: |