0

0

深入理解TypeScript递归类型:构建深层可写属性并规避深度限制

碧海醫心

碧海醫心

发布时间:2025-10-29 18:36:22

|

590人浏览过

|

来源于php中文网

原创

深入理解TypeScript递归类型:构建深层可写属性并规避深度限制

本文深入探讨了在typescript中构建一个能够递归地提取类字段属性、排除函数、并正确处理可选性及各种嵌套数据结构(如对象、数组、map、set)的深层可写(deepwritable)类型。文章详细分析了导致“类型实例化深度过大”错误的原因,并提供了一种优化后的解决方案,确保类型安全和性能。

TypeScript深层可写类型:递归属性提取与深度限制规避

在TypeScript中,我们经常需要处理复杂的数据结构,特别是在需要修改对象部分属性时,保持类型安全至关重要。一个常见的需求是创建一个“深层可写”类型,它能够递归地遍历一个类的所有可写字段,同时排除方法(函数),并正确处理字段的可选性。然而,在实现此类递归类型时,我们可能会遇到TypeScript的“类型实例化深度过大且可能无限”(Type instantiation is excessively deep and possibly infinite)错误。本文将详细解析这个问题,并提供一个健壮的解决方案。

问题背景与挑战

我们的目标是为类的 set 方法提供一个类型安全的参数,该参数允许我们更新类的部分属性,且这些属性必须是可写的,并能深入到嵌套的对象、数组、Map和Set中。

初始尝试通常会涉及以下几个关键类型:

  1. IfEquals<X, Y, A, B>: 一个用于比较两个类型是否完全相等的辅助类型。
  2. WritableKeys<T>: 筛选出类型 T 中非函数且非只读的属性键。
  3. DeepWritable<T>: 核心递归类型,用于将 T 的所有可写属性转换为深层可写形式。

然而,在实现 DeepWritable 时,尤其是当它需要处理 Map、Set、数组以及普通对象等多种递归结构时,很容易触及TypeScript的类型检查深度限制,导致上述错误。

核心辅助类型

在深入解决递归问题之前,我们先定义一些基础的辅助类型:

// 1. IfEquals: 比较两个类型是否完全相等
type IfEquals<X, Y, A = X, B = never> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? A : B;

// 2. WritableKeys: 提取类型T中可写的(非函数、非只读)属性键
type WritableKeys<T> = {
  [P in keyof T]: T[P] extends Function ? never : IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P>
}[keyof T];

// 3. DeepWritablePrimitive: 定义深层可写类型中的基本类型
type DeepWritablePrimitive = undefined | null | boolean | string | number | Function;

WritableKeys 通过 IfEquals 巧妙地判断属性是否为只读,并通过 T[P] extends Function ? never : ... 排除函数类型。

解决“类型实例化深度过大”错误

原始的 DeepWritable 类型在处理 Map 和 Set 等复杂泛型时,可能因为检查顺序或内部实现方式导致递归深度过大。为了解决这个问题,我们需要优化 DeepWritable 的结构,特别是对不同数据结构的判断顺序。

AI小聚
AI小聚

一站式多功能AIGC创作平台,支持AI绘画、AI视频、AI聊天、AI音乐

下载

关键的改进点在于:

  1. Map类型的优先处理:将 Map<any, any> 的检查放在 T extends (infer U)[] 之后但在 DeepWritableRecord 之前。这是因为 Map 类型本身是一个泛型接口,如果处理不当,可能导致编译器在递归解析时陷入困境。通过先检查 Map<any, any>,我们可以更明确地引导编译器。
  2. 正确处理可选性:原始的 DeepWritableObject 在处理 WritableKeys 时可能会丢失属性的可选性。我们需要确保在构造新类型时保留原始属性的可选状态。

下面是优化后的 DeepWritable 及其辅助类型 DeepWritableRecord:

// 优化后的 DeepWritable 类型
type DeepWritable<T> =
  | T extends DeepWritablePrimitive ? T // 基本类型直接返回
  : T extends (infer U)[] ? DeepWritable<U>[] // 数组递归处理元素
  : T extends Map<any, any> ? ( // 优先处理 Map 类型
      T extends Map<infer K, infer V> ? Map<K, DeepWritable<V>> : never
    )
  : T extends Set<infer V> ? Set<DeepWritable<V>> // Set 递归处理元素
  : DeepWritableRecord<T>; // 其他对象类型递归处理

// 优化后的 DeepWritableRecord 类型,保留可选性
type DeepWritableRecord<T> = {
  // 使用 Pick 筛选出可写键,并保留其原始的可选性
  [K in keyof Pick<T, WritableKeys<T>>]: DeepWritable<T[K]>
}

解释:

  • DeepWritable 类型现在首先检查基本类型,然后是数组。
  • 关键点:Map<any, any> 的检查被提前。如果一个类型 T 是 Map 的实例,它会通过 Map<infer K, infer V> 提取键 K 和值 V 的类型,然后递归地将值 V 转换为 DeepWritable<V>。
  • Set 类型的处理紧随其后。
  • 最后,对于所有不匹配上述条件的类型,我们将其视为普通对象,并委托给 DeepWritableRecord<T>。
  • DeepWritableRecord<T> 使用 Pick<T, WritableKeys<T>> 来首先从原始类型 T 中选择那些可写的属性键,这样可以保留这些属性的原始可选性。然后,它递归地将这些属性的值转换为 DeepWritable<T[K]>。

完整示例代码

让我们将这些类型应用到一个实际的类结构中:

// 辅助类型定义 (如上所示)
type IfEquals<X, Y, A=X, B=never> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? A : B;

type WritableKeys<T> = {
  [P in keyof T]: T[P] extends Function ? never : IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P>
}[keyof T];

type DeepWritablePrimitive = undefined | null | boolean | string | number | Function;

// 优化后的 DeepWritable 类型定义 (如上所示)
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 DeepWritableRecord<T> = {
  [K in keyof Pick<T, WritableKeys<T>>]: DeepWritable<T[K]>
}

// 示例类结构
class Base {
  // set 方法接受一个 Partial<DeepWritable<typeof this>> 类型的参数
  set(data?: Partial<DeepWritable<typeof this>>) {
    Object.assign(this, data);
  }
}

class Parent extends Base {
  name?: string; // 可选属性
  age: number = 0; // 必选属性
  readonly id: string = 'some-id'; // 只读属性,应被排除
  private _secret: string = 'secret'; // 私有属性,类型系统通常不直接处理,但如果通过公共方法暴露则需注意
  someMethod() { return 'hello'; } // 方法,应被排除
  arr?: Parent[]; // 嵌套数组
  dataMap?: Map<string, Parent>; // 嵌套 Map
  dataList?: Set<string>; // 嵌套 Set
}

const record = new Parent();

// 使用 set 方法更新属性
record.set({
  name: 'New Name', // 更新可选属性
  age: 30, // 更新必选属性
  // id: 'new-id', // 错误:'id' 是只读属性,已被 WritableKeys 排除
  // someMethod: () => 'new hello', // 错误:'someMethod' 是函数,已被 WritableKeys 排除
  arr: [{ // 嵌套数组
    name: 'Child 1',
    age: 5
  }, {
    name: 'Child 2',
    arr: [{ name: 'Grandchild 1' }] // 更深层次的嵌套
  }],
  dataMap: new Map([ // 嵌套 Map
    ['key1', { name: 'Map Child 1', age: 10 }],
    ['key2', { name: 'Map Child 2' }]
  ]),
  dataList: new Set(['item1', 'item2']) // 嵌套 Set
});

console.log(record);
console.log(record.arr?.[0]?.name); // Child 1
console.log(record.arr?.[1]?.arr?.[0]?.name); // Grandchild 1
console.log(record.dataMap?.get('key1')?.name); // Map Child 1

// 验证类型检查:尝试设置只读属性或方法会报错
// record.set({ id: 'test' }); // Error: Type '{ id: string; }' has no properties in common with type 'Partial<DeepWritable<Parent>>'.
// record.set({ someMethod: () => {} }); // Error: Type '{ someMethod: () => void; }' has no properties in common with type 'Partial<DeepWritable<Parent>>'.

注意事项与总结

  1. 类型检查顺序至关重要:在递归类型中,对不同数据结构的检查顺序会影响TypeScript的类型推断性能和是否触发深度限制。将 Map 等复杂泛型放在数组之后、普通对象之前处理,通常能有效规避“深度过大”错误。
  2. 保留可选性:使用 Pick<T, WritableKeys<T>> 结合 keyof 是一个优雅的方式,既能筛选出可写属性,又能保持这些属性在原始类型中的可选性。
  3. DeepWritablePrimitive 的作用:明确定义基本类型可以作为递归的终止条件,防止无限递归。
  4. 只读和函数属性的排除:WritableKeys 类型确保了我们只处理可修改的字段,提高了类型安全性。
  5. 私有属性:TypeScript的类型系统通常只关注公共(public)和受保护(protected)的属性。私有属性 (private) 在类型层面通常不会被 keyof T 捕获,因此不会被 DeepWritable 处理。

通过上述优化,我们成功构建了一个健壮的 DeepWritable 类型,它不仅能够递归地处理复杂的嵌套结构,排除不必要的属性,还能有效避免TypeScript的类型实例化深度限制,为应用程序提供了更强大的类型安全保障。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
TypeScript工程化开发与Vite构建优化实践
TypeScript工程化开发与Vite构建优化实践

本专题面向前端开发者,深入讲解 TypeScript 类型系统与大型项目结构设计方法,并结合 Vite 构建工具优化前端工程化流程。内容包括模块化设计、类型声明管理、代码分割、热更新原理以及构建性能调优。通过完整项目示例,帮助开发者提升代码可维护性与开发效率。

47

2026.02.13

TypeScript全栈项目架构与接口规范设计
TypeScript全栈项目架构与接口规范设计

本专题面向全栈开发者,系统讲解基于 TypeScript 构建前后端统一技术栈的工程化实践。内容涵盖项目分层设计、接口协议规范、类型共享机制、错误码体系设计、接口自动化生成与文档维护方案。通过完整项目示例,帮助开发者构建结构清晰、类型安全、易维护的现代全栈应用架构。

194

2026.02.25

treenode的用法
treenode的用法

​在计算机编程领域,TreeNode是一种常见的数据结构,通常用于构建树形结构。在不同的编程语言中,TreeNode可能有不同的实现方式和用法,通常用于表示树的节点信息。更多关于treenode相关问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

549

2023.12.01

C++ 高效算法与数据结构
C++ 高效算法与数据结构

本专题讲解 C++ 中常用算法与数据结构的实现与优化,涵盖排序算法(快速排序、归并排序)、查找算法、图算法、动态规划、贪心算法等,并结合实际案例分析如何选择最优算法来提高程序效率。通过深入理解数据结构(链表、树、堆、哈希表等),帮助开发者提升 在复杂应用中的算法设计与性能优化能力。

30

2025.12.22

深入理解算法:高效算法与数据结构专题
深入理解算法:高效算法与数据结构专题

本专题专注于算法与数据结构的核心概念,适合想深入理解并提升编程能力的开发者。专题内容包括常见数据结构的实现与应用,如数组、链表、栈、队列、哈希表、树、图等;以及高效的排序算法、搜索算法、动态规划等经典算法。通过详细的讲解与复杂度分析,帮助开发者不仅能熟练运用这些基础知识,还能在实际编程中优化性能,提高代码的执行效率。本专题适合准备面试的开发者,也适合希望提高算法思维的编程爱好者。

44

2026.01.06

硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

1926

2023.10.19

PHP接口编写教程
PHP接口编写教程

本专题整合了PHP接口编写教程,阅读专题下面的文章了解更多详细内容。

656

2025.10.17

php8.4实现接口限流的教程
php8.4实现接口限流的教程

PHP8.4本身不内置限流功能,需借助Redis(令牌桶)或Swoole(漏桶)实现;文件锁因I/O瓶颈、无跨机共享、秒级精度等缺陷不适用高并发场景。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2399

2025.12.29

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

76

2026.03.11

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
TypeScript 教程
TypeScript 教程

共19课时 | 3.4万人学习

TypeScript——十天技能课堂
TypeScript——十天技能课堂

共21课时 | 1.2万人学习

TypeScript-45分钟入门
TypeScript-45分钟入门

共6课时 | 0.5万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号