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

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

核心辅助类型

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

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

// 2. WritableKeys: 提取类型T中可写的(非函数、非只读)属性键
type WritableKeys = {
  [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 的结构,特别是对不同数据结构的判断顺序。

Groq
Groq

GroqChat是一个全新的AI聊天机器人平台,支持多种大模型语言,可以免费在线使用。

下载

关键的改进点在于:

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

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

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

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

解释:

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

完整示例代码

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

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

type WritableKeys = {
  [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 extends DeepWritablePrimitive ? T
  : T extends (infer U)[] ? DeepWritable[]
  : T extends Map ? (
      T extends Map ? Map> : never
    )
  : T extends Set ? Set>
  : DeepWritableRecord;

type DeepWritableRecord = {
  [K in keyof Pick>]: DeepWritable
}

// 示例类结构
class Base {
  // set 方法接受一个 Partial> 类型的参数
  set(data?: Partial>) {
    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; // 嵌套 Map
  dataList?: Set; // 嵌套 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>'.
// record.set({ someMethod: () => {} }); // Error: Type '{ someMethod: () => void; }' has no properties in common with type 'Partial>'.

注意事项与总结

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

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

相关专题

更多
treenode的用法
treenode的用法

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

529

2023.12.01

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

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

11

2025.12.22

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

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

991

2023.10.19

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

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

51

2025.10.17

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

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

232

2025.12.29

golang map内存释放
golang map内存释放

本专题整合了golang map内存相关教程,阅读专题下面的文章了解更多相关内容。

73

2025.09.05

golang map相关教程
golang map相关教程

本专题整合了golang map相关教程,阅读专题下面的文章了解更多详细内容。

25

2025.11.16

golang map原理
golang map原理

本专题整合了golang map相关内容,阅读专题下面的文章了解更多详细内容。

36

2025.11.17

php源码安装教程大全
php源码安装教程大全

本专题整合了php源码安装教程,阅读专题下面的文章了解更多详细内容。

74

2025.12.31

热门下载

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

精品课程

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

共19课时 | 1.9万人学习

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

共21课时 | 1.1万人学习

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

共6课时 | 0.4万人学习

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

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