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

本文深入探讨了在typescript中构建一个能够递归地提取类字段属性、排除函数、并正确处理可选性及各种嵌套数据结构(如对象、数组、map、set)的深层可写(deepwritable)类型。文章详细分析了导致“类型实例化深度过大”错误的原因,并提供了一种优化后的解决方案,确保类型安全和性能。
TypeScript深层可写类型:递归属性提取与深度限制规避
在TypeScript中,我们经常需要处理复杂的数据结构,特别是在需要修改对象部分属性时,保持类型安全至关重要。一个常见的需求是创建一个“深层可写”类型,它能够递归地遍历一个类的所有可写字段,同时排除方法(函数),并正确处理字段的可选性。然而,在实现此类递归类型时,我们可能会遇到TypeScript的“类型实例化深度过大且可能无限”(Type instantiation is excessively deep and possibly infinite)错误。本文将详细解析这个问题,并提供一个健壮的解决方案。
问题背景与挑战
我们的目标是为类的 set 方法提供一个类型安全的参数,该参数允许我们更新类的部分属性,且这些属性必须是可写的,并能深入到嵌套的对象、数组、Map和Set中。
初始尝试通常会涉及以下几个关键类型:
-
IfEquals
: 一个用于比较两个类型是否完全相等的辅助类型。 -
WritableKeys
: 筛选出类型 T 中非函数且非只读的属性键。 -
DeepWritable
: 核心递归类型,用于将 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电商营销工具
168
查看详情
关键的改进点在于:
- Map类型的优先处理:将 Map 的检查放在 T extends (infer U)[] 之后但在 DeepWritableRecord 之前。这是因为 Map 类型本身是一个泛型接口,如果处理不当,可能导致编译器在递归解析时陷入困境。通过先检查 Map,我们可以更明确地引导编译器。
- 正确处理可选性:原始的 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 的检查被提前。如果一个类型 T 是 Map 的实例,它会通过 Map
提取键 K 和值 V 的类型,然后递归地将值 V 转换为 DeepWritable 。 - Set 类型的处理紧随其后。
- 最后,对于所有不匹配上述条件的类型,我们将其视为普通对象,并委托给 DeepWritableRecord
。 - DeepWritableRecord
使用 Pick > 来首先从原始类型 T 中选择那些可写的属性键,这样可以保留这些属性的原始可选性。然后,它递归地将这些属性的值转换为 DeepWritable 。
完整示例代码
让我们将这些类型应用到一个实际的类结构中:
// 辅助类型定义 (如上所示)
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>>'.注意事项与总结
- 类型检查顺序至关重要:在递归类型中,对不同数据结构的检查顺序会影响TypeScript的类型推断性能和是否触发深度限制。将 Map 等复杂泛型放在数组之后、普通对象之前处理,通常能有效规避“深度过大”错误。
-
保留可选性:使用 Pick
> 结合 keyof 是一个优雅的方式,既能筛选出可写属性,又能保持这些属性在原始类型中的可选性。 - DeepWritablePrimitive 的作用:明确定义基本类型可以作为递归的终止条件,防止无限递归。
- 只读和函数属性的排除:WritableKeys 类型确保了我们只处理可修改的字段,提高了类型安全性。
- 私有属性:TypeScript的类型系统通常只关注公共(public)和受保护(protected)的属性。私有属性 (private) 在类型层面通常不会被 keyof T 捕获,因此不会被 DeepWritable 处理。
通过上述优化,我们成功构建了一个健壮的 DeepWritable 类型,它不仅能够递归地处理复杂的嵌套结构,排除不必要的属性,还能有效避免TypeScript的类型实例化深度限制,为应用程序提供了更强大的类型安全保障。
以上就是深入理解TypeScript递归类型:构建深层可写属性并规避深度限制的详细内容,更多请关注其它相关文章!
# 所示
# 孟州建设网站
# 杭州鲜花网站推荐优化
# 抖音电竞营销怎么做推广
# 绍兴新站seo排名
# 大兴高档网站建设
# 汉服网店网站建设方案
# 怀化360营销推广方法
# 河西区线上营销推广方案
# 网络营销主动推广案例分析
# 博客和推广seo优化
# typescript
# 已被
# 正确处理
# 放在
# 转换为
# 是一个
# 过大
# 数据结构
# 可选
# 递归
相关栏目:
【
科技资讯46185 】
【
网络学院92790 】
相关推荐:
HTML空白字符处理机制:渲染、DOM与编码实践
极兔快递快件信息查询系统 极兔快递官网运单号追踪
Django表单提交验证失败后保持字段值不刷新
《燕云十六声》两周内达九百万玩家!位居畅销榜第五
Golang如何实现简单的Web表单_Golang表单提交与验证处理方法
cad怎么合并重叠的线段_cad清理重复重叠线条的操作方法
Lar*el如何正确地在控制器和模型之间分配逻辑_Lar*el代码职责分离与架构建议
《刺客信条4:黑旗》重制版新细节曝光:无缝加载 地图更细致!
Python大型XML文件高效流式解析教程
LINUX下如何进行磁盘分区_fdisk与parted工具在LINUX中的使用对比
如何修改开机登录密码_Windows账户安全设置超详细教程【必学】
提升Kafka消费者健壮性:会话超时处理与消息处理语义
Yandex免登录网页版地址 Yandex搜索引擎官方访问入口
MAC如何将整个网页截长图_MAC使用Safari的导出为PDF或第三方工具
MAC怎么让Dock栏只显示当前运行的应用_MAC终端命令实现极简Dock栏
我的世界官方游戏入口 我的世界官网平台直达链接
Mac怎么查看崩溃日志_Mac控制台错误报告分析
Pygame教程:解决用户输入与游戏状态更新不同步问题
fishbowl官网免费版 fishbowl养鱼网站入口
邮政编码查询不到怎么办_邮政编码查询不到的常见原因与对策
poki网页游戏推荐_poki免费游戏平台入口
C++如何使用AddressSanitizer(ASan)_C++调试工具中检测内存访问错误的利器
React项目中导航栏Logo自适应布局:避免裁剪与布局溢出
PHP URL参数传递与500错误调试指南
C#使用XPath查询节点时出错? 常见语法错误与调试技巧
CKEditor 5 自定义构建在React应用中渲染失败的调试与解决
必由学官方平台入口 必由学在线课堂登录地址
Safari浏览器输入栏卡顿如何解决 Safari搜索建议与缓存清理
win11 Snap Layouts怎么用 Win11窗口布局与分屏多任务高效指南【必学】
AO3最新可访问网址 Archive of Our Own官方在线入口
word中如何让数字纵向排列_Word数字纵向排列方法
qq游戏手机版下载安装_qq游戏移动端入口
126邮箱网页版官方入口 126邮箱账号在线登录平台
深入理解J*aScript Promise异步执行与微任务队列
Win10系统服务哪些可以禁用 Win10安全优化服务列表【干货】
京东京造J1和网易云音乐氧气真无线有什么不同_国产电商蓝牙耳机音质对比
照顾宝贝2小游戏点击立即在线玩
Yandex搜索引擎一键访问入口_俄罗斯Yandex官网免登录
J*aScript生成器_j*ascript异步迭代
支付宝如何设置安全保护_支付宝安全设置的全面教程
QQ邮箱登录平台入口 QQ邮箱网页版邮箱官方入口
Steam官网入口直达 Steam注册及登录步骤
极速漫画官方主页网址 极速漫画漫画在线浏览官网链接
PyTorch模型训练效果不佳?深入剖析常见错误与调试技巧
AO3官方可用镜像 Archive of Our Own网页版最新入口
妖精漫画网页版登录入口免费_妖精漫画官网主页直接阅读漫画
如何使用Rector自动化升级旧代码_通过Composer安装和配置Rector进行代码重构
谷歌推RCS信息存档功能:公司可监控员工私密信息!
J*aScript map 方法中处理循环元素为空数组的策略
Python:递归比较文件夹内容并找出特定类型文件的差异


2025-10-29
浏览次数:次
返回列表
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>>'.