在TypeScript中创建递归结构时如何避免过度深层类型实例化?
解决TypeScript 4.9.4中递归类型的过度深层实例化问题
问题重现
以下是一个用于验证source是否符合schema结构的类型化函数示例,当在函数体内使用source时,会触发TS2589错误:“Type instantiation is excessively deep and possibly infinite”。
// TypeScript v4.9.4 type SchemaMeta = { type: SchemaAll, extra?: boolean }; type Schema1 = 1; type Schema2 = 2; type SchemaAll = Schema1 | Schema2 | SchemaMeta; type Source<Schema extends SchemaAll> = ( Schema extends Schema1 ? "String Option #1" : Schema extends Schema2 ? "String Option #2" : Schema extends SchemaMeta ? Source<Schema["type"]> : never ); function validate<Schema extends SchemaAll>(schema: Schema, source: Source<Schema>): void { // 错误:TS2589: Type instantiation is excessively deep and possibly infinite. source; } // 正确的调用示例 validate({ type: 1 }, "String Option #1"); validate({ type: { type: 2, extra: false } }, "String Option #2"); // 正确的类型校验错误 validate(2, "String Option #1"); // Argument of type '"String Option #1"' is not assignable to parameter of type '"String Option #2"'.
该逻辑要求:
- 当
schema为1时,source必须是"String Option #1" - 当
schema为2时,source必须是"String Option #2" schema也可以是嵌套的SchemaMeta对象,其type字段支持自引用
原因分析
TypeScript 4.9.4对递归类型的实例化没有完善的尾递归优化机制。当函数体内需要解析Source<Schema>类型时,由于SchemaMeta的自引用特性,TypeScript会尝试无限递归展开Source<Schema["type"]>,直到达到类型实例化的深度上限,最终触发TS2589错误。
解决方案:添加递归深度限制
通过给Source类型引入一个可选的深度参数,手动限制递归展开的次数,避免无限递归。具体实现如下:
// TypeScript v4.9.4 type SchemaMeta = { type: SchemaAll, extra?: boolean }; type Schema1 = 1; type Schema2 = 2; type SchemaAll = Schema1 | Schema2 | SchemaMeta; // 引入Depth参数限制递归深度,默认值10可根据需求调整 type Source<Schema extends SchemaAll, Depth extends number = 10> = Depth extends 0 ? never : // 深度耗尽时终止递归 Schema extends Schema1 ? "String Option #1" : Schema extends Schema2 ? "String Option #2" : Schema extends SchemaMeta ? Source<Schema["type"], [never, ...Array<unknown>][Depth]> : // 深度减1 never; function validate<Schema extends SchemaAll>(schema: Schema, source: Source<Schema>): void { // 现在可正常编译 source; } // 原有测试用例依然正常工作 validate({ type: 1 }, "String Option #1"); validate({ type: { type: 2, extra: false } }, "String Option #2"); // 类型校验错误提示保持有效 validate(2, "String Option #1"); // Argument of type '"String Option #1"' is not assignable to parameter of type '"String Option #2"'.
方案说明
- 利用
Depth参数控制递归次数,默认设置为10(可根据实际嵌套深度需求调整) - 通过
[never, ...Array<unknown>][Depth]实现深度减1的效果(数组长度技巧,将数字类型转换为长度减1的数组对应的数字类型) - 当深度耗尽(
Depth extends 0)时返回never,终止递归展开
替代方案:函数体内规避类型检查
如果不需要在函数体内对source进行复杂的类型操作,也可以通过类型注释或断言临时规避检查,但这种方式不推荐用于需要严格类型校验的场景:
function validate<Schema extends SchemaAll>(schema: Schema, source: Source<Schema>): void { // @ts-ignore 临时跳过类型检查 source; }
或者使用类型断言:
function validate<Schema extends SchemaAll>(schema: Schema, source: Source<Schema>): void { const _source = source as unknown; // 转换为unknown避免递归解析 }
内容的提问来源于stack exchange,提问作者Jeremy
相关产品推荐
相关产品推荐

