
本文旨在解决 typescript 中递归获取类字段属性时遇到的 “Type instantiation is excessively deep and possibly infinite” 错误。通过修改类型定义,特别是针对 `map` 类型的特殊处理,以及保留字段的可选性,提供了一种避免无限递归并正确获取字段类型的方法。
在 TypeScript 中,使用递归类型定义来处理复杂的数据结构是很常见的。然而,不当的递归类型定义可能导致编译器报错,提示 “Type instantiation is excessively deep and possibly infinite”。这个问题通常发生在类型定义过于复杂,导致编译器无法在合理的深度内完成类型推断时。本文将探讨如何解决这个问题,并提供一个具体的示例,展示如何递归地获取类字段的属性,同时排除函数类型,并保留字段的可选性。
理解问题根源
“Type instantiation is excessively deep and possibly infinite” 错误通常是由于类型定义中的循环依赖造成的。例如,一个类型 A 的定义依赖于类型 B,而类型 B 的定义又依赖于类型 A。如果这种依赖关系没有明确的终止条件,编译器就会陷入无限递归,最终报错。
在处理类字段属性时,如果类包含嵌套的复杂类型(例如,数组、Map、Set 等),并且递归类型定义没有针对这些类型进行特殊处理,就很容易触发这个错误。
解决方案:改进递归类型定义
为了解决这个问题,我们需要改进递归类型定义,使其能够正确处理各种类型,并避免无限递归。以下是一种可行的解决方案:
- 限制递归深度: 虽然 TypeScript 本身没有直接限制递归深度的机制,但可以通过一些技巧来间接实现。例如,可以添加一个类型参数来表示递归深度,并在每次递归调用时递减该参数。当参数达到某个阈值时,停止递归。
- 特殊处理 Map 类型: Map 类型是导致无限递归的常见原因。我们需要在递归类型定义中显式地检查 Map 类型,并对其进行特殊处理。一种方法是先检查类型是否为 Map<any, any>,如果是,则进一步推断键和值的类型,并对值类型进行递归处理。
- 保留字段的可选性: 在获取类字段属性时,我们需要保留字段的可选性。这意味着如果一个字段是可选的(使用 ? 标记),那么在递归类型定义中也应该保持其可选性。
- 明确终止条件: 递归类型定义必须有一个明确的终止条件。例如,当类型为原始类型(如 String、number、Boolean 等)时,停止递归。
代码示例
以下是一个改进后的代码示例,展示了如何递归地获取类字段的属性,同时排除函数类型,并保留字段的可选性:
type DeepWritablePrimitive = undefined | null | boolean | string | number | Function; type DeepWritable<T> = | T extends DeepWritablePrimitive ? T : T extends (infer U)[] ? DeepWritable<U>[] : T extends Map<any, any> ? ( T extends Map<infer K, infer V> ? Map<K, DeepWritable<V>> : never ) : T extends Set<infer V> ? Set<DeepWritable<V>> : DeepWritableRecord<T>; type WritableKeys<T> = { [P in keyof T]: T[P] extends Function ? never : P }[keyof T]; type DeepWritableRecord<T> = { // need to keep optionality [K in keyof Pick<T, WritableKeys<T>>]: DeepWritable<T[K]> } class Base { set(data?: Partial<DeepWritable<typeof this>>) { Object.assign(this, data); } } class Parent extends Base { name?: string; arr?: Parent[]; map?: Map<string, Parent>; }; const record = new Parent(); record.set({ // https://github.com/microsoft/TypeScript/issues/34933 arr: [{ name: '0' }], map: new Map([['key', {name: 'map_value'}]]) }) console.log(record.arr); console.log(record.map?.get('key')?.name);
在这个示例中,DeepWritable 类型定义递归地处理了数组、Map 和 Set 类型,并保留了字段的可选性。WritableKeys 用于排除函数类型的字段。
注意事项
- 在处理复杂的类型定义时,建议逐步构建类型,并使用 TypeScript 的类型检查器来验证类型的正确性。
- 如果仍然遇到 “Type instantiation is excessively deep and possibly infinite” 错误,可以尝试简化类型定义,或者使用类型别名来减少类型的嵌套深度。
- 在某些情况下,可能需要使用类型断言来绕过类型检查器的限制。但是,应该谨慎使用类型断言,因为它可能会导致运行时错误。
总结
通过改进递归类型定义,我们可以解决 TypeScript 中 “Type instantiation is excessively deep and possibly infinite” 错误,并正确地获取类字段的属性。关键在于限制递归深度,特殊处理 Map 类型,保留字段的可选性,并明确终止条件。希望本文能够帮助你更好地理解和解决这个问题。


