
示例工程【免费下载链接】type-challengesCollection of TypeScript type challenges with online judge项目地址https://gitcode.com/GitHub_Trending/ty/type-challenges点击查看免费下载本指南以 type-challenges 仓库第 9 题「Deep Readonly深度只读」为核心系统讲解如何用 TypeScript 类型系统实现一个递归地将对象及其全部子对象属性置为readonly的泛型工具类型。通过阅读本文你将掌握映射类型Mapped Types、递归类型Recursive Types、条件类型分发Distributive Conditional Types在深度嵌套场景中的组合运用并能结合仓库内的测试用例验证实现的正误。挑战概览题目信息与定位「Deep Readonly」是 type-challenges 题库中的第 9 题作者是 Anthony Fu 可以确认其官方信息标题Deep Readonly难度medium中等标签readonly、object-keys、deep关联题目7Readonly、8Readonly 2它处于「只读系列」的进阶链条上第 7 题要求将对象所有属性置为只读浅层第 8 题要求可按K指定部分属性置为只读而第 9 题则把只读操作递归地下沉到每一层子对象。因此理解本题之前建议先完成 00007-easy-readonly 与 00008-medium-readonly-2 两题它们是本体的直接铺垫。题目要求精读题目的核心任务韩文版原文 README.ko.md翻译如下实现一个泛型DeepReadonlyT它将对象的每个参数property及其所有子对象递归地设为只读。在本挑战中可以假设我们只处理对象类型数组、函数、类等不需要考虑。不过你仍然可以通过覆盖尽可能多的不同类型来挑战自己。题目给出的参考示例如下type X { x: { a: 1 b: hi } y: hey } type Expected { readonly x: { readonly a: 1 readonly b: hi } readonly y: hey } type Todo DeepReadonlyX // should be same as Expected注意Expected的类型结构不仅顶层x、y变成了readonly嵌套的a、b也被递归地加上了readonly修饰符。这意味着DeepReadonly不能只做一层keyof映射必须递归地进入每个属性值T[K]内部继续处理。仓库中的起点模板 template.ts 只有一行占位type DeepReadonlyT any你的任务就是把any替换为真正的类型逻辑。从浅到深四步推导标准解法第一步先写出浅层只读如果你已经做过第 7 题 00007-easy-readonly会知道浅层只读的写法是利用映射类型对T的每个键加readonly修饰符type MyReadonlyT { readonly [K in keyof T]: T[K] }这套语法是 TypeScript 内置ReadonlyT的实现方式keyof T取出对象的所有键T[K]取出对应值类型。第二步引入递归浅层只读只处理了第一层。要让子对象内部的属性也变成readonly最自然的思路是对属性值T[K]也应用DeepReadonly形成自引用type DeepReadonlyT { readonly [K in keyof T]: DeepReadonlyT[K] }这已经能通过题目的基础示例。TypeScript 允许类型别名自引用类型求值会在需要时展开递归这正是类型层面的“深度遍历”。第三步处理数组与元组题目强调“只考虑对象”但仓库的测试用例 test-cases.ts 事实上覆盖了数组/元组和联合类型。测试类型X1中包含l: [ hi, { m: [hey] }, ]对应的期望结果Expected1是readonly l: readonly [ hi, { readonly m: readonly [hey] }, ]这里有两个关键细节数组/元组本身被变成了readonly元组readonly [...]元组元素中的对象{ m: [hey] }也被递归处理其内部m数组同样变成readonly [hey]。问题在于直接对数组应用{ readonly [K in keyof T]: DeepReadonlyT[K] }时keyof会映射出下标0 | 1 | ...及length等虽然能得到一个“可读索引但不可写元素”的近似效果但结果的数组方法如push依旧存在且元组的长度信息会被抹掉无法匹配readonly [...]的期望形态。更稳妥的写法是用条件类型区分数组并对数组元素递归应用type DeepReadonlyT T extends (infer R)[] ? DeepReadonlyR[] : T extends (...args: any[]) any ? T : { readonly [K in keyof T]: DeepReadonlyT[K] }如果希望元组形态和readonly修饰符都被完整保留可以拆开readonly数组与非只读数组两种情况分别递归type DeepReadonlyT T extends readonly (infer R)[] ? readonly DeepReadonlyR[] : T extends (...args: any[]) any ? T : { readonly [K in keyof T]: DeepReadonlyT[K] }从源码结构看测试用例 test-cases.ts 期望l变成readonly [...]元组且保留字面量元素顺序因此保留元组形态的实现才能通过Equal严格相等校验见下文测试机制。第四步处理联合类型测试用例还给出了第二个用例type X2 { a: string } | { b: number } type Expected2 { readonly a: string } | { readonly b: number }即DeepReadonly必须能处理联合类型对联合的每个成员分别递归地只读化。这依赖 TypeScript 条件类型的一个特性当条件类型左侧是裸类型参数naked type parameter时会对联合类型自动分发distribute。如果我们把DeepReadonlyT写成T extends ... ? ... : { readonly [K in keyof T]: ... }这种以裸T为判断对象的条件类型那么传入{ a: string } | { b: number }时T会被逐个拆开为{ a: string }和{ b: number }分别求值最终得到{ readonly a: string } | { readonly b: number }与Expected2完全一致。这正是标准解法能够通过全部测试的关键机制。测试用例源码印证行为边界从何而来仓库的 test-cases.ts 是本体的权威行为规范值得逐层分析import type { Equal, Expect } from type-challenges/utils type cases [ ExpectEqualDeepReadonlyX1, Expected1, ExpectEqualDeepReadonlyX2, Expected2, ]ExpectEqual...是 type-challenges 的断言工具定义在 utils/index.d.ts 中。EqualX, Y通过函数类型参数逆变比较两个类型是否结构上完全相等ExpectT extends true强制T为字面量true任何一处不匹配例如缺少readonly、元组被展平为数组、字面量类型被拓宽都会导致编译报错。这保证了我们的实现必须与期望类型逐字节对齐而不是“结构兼容即可”。X1是一个 4 层深的嵌套对象c → e → g → h → i/j其中混有函数属性a: () 22、字符串、布尔、对象数组嵌套用于检验递归深度与数组处理X2是联合类型用于检验条件类型分发行为。X1的期望输出Expected1中有两个值得注意的点函数属性a: () 22保持原样没有被包进映射对象也没有被加上readonly——因为函数本身不可“只读化”应当原样透传数组l与m被转换为readonly元组readonly [...]同时元素中的对象继续递归只读化。这两个点决定了实现必须在条件类型中先拦截函数T extends (...args: any[]) any ? T : ...再拦截数组最后才走对象映射分支。顺序颠倒会导致函数被错误地映射、或数组被当作普通对象处理而丢失元组信息。解法演进从最小实现到完备实现下面给出一个可以完整通过本仓库两个测试用例的参考实现type DeepReadonlyT T extends (...args: any[]) any ? T : T extends readonly (infer R)[] ? readonly DeepReadonlyR[] : { readonly [K in keyof T]: DeepReadonlyT[K] }逐步拆解分支条件处理方式函数T extends (...args: any[]) any原样返回T避免被映射破坏签名数组/元组T extends readonly (infer R)[]提取元素类型R递归DeepReadonlyR后包回readonly R[]元组对象兜底分支映射每个键并加readonly属性值递归处理由于第 2 个分支的左侧T是裸类型参数条件类型会自动对联合类型分发因此X2这类{ a: string } | { b: number }输入会被逐个成员处理最终返回{ readonly a: string } | { readonly b: number }无需额外代码。如果要保守地只满足题目最低要求只处理对象最小实现就是type DeepReadonlyT { readonly [K in keyof T]: DeepReadonlyT[K] }但这个版本无法通过X1的数组用例元组信息丢失、缺少readonly数组修饰也无法正确处理函数属性因此实际提交时应采用上面的完备版本。边界情况与深度陷阱在围绕本体练习时以下几类输入值得专门验证原始类型与字面量string、number、hi、true等没有keyof键或键为空映射后仍是原类型天然满足只读语义函数与类() 22、class Foo {}应原样透传。函数分支用(...args: any[]) any拦截即可类类型本质上是带构造签名与原型链的复杂对象如需“尽可能覆盖”可以进一步用T extends abstract new (...args: any[]) any拦截构造签名但测试用例并未要求Date、Map、Set等内置类若不拦截会被映射类型拆解成普通对象形态破坏实例语义。题目声明“只考虑对象”这类输入属于可选的挑战性扩展联合类型的分发条件类型分发只发生在裸类型参数上。如果实现中把T包进了T[]或keyof T等复合形态分发就不会发生联合用例将直接失败递归深度与性能极深嵌套几十层可能显著增加类型实例化开销这是所有递归类型共有的权衡。仓库测试的X1深度约 4 层属于常规范围。与第 7、8 题的对比只读系列的能力演进把三题放在一起看能更清晰地理解本题的设计意图题目目标核心语法7・Readonly所有属性浅层只读{ readonly [K in keyof T]: T[K] }8・Readonly 2按K选择部分属性只读映射类型 K extends keyof T条件分支9・Deep Readonly所有属性递归只读自引用映射 条件类型分发 数组/函数拦截从 info.yml 的related: 7, 8字段也可以确认官方把这组题视为同一个系列。第 8 题 template.ts 的占位签名MyReadonly2T, K引入了第二个类型参数用来在映射中区分“需要只读的键”与“保持原样的键”而本题则把关注点从“选哪些键”转移到“递归多深”两者互补一起构成对映射类型能力的完整训练。总结DeepReadonly是 type-challenges 中一道典型的“中等难度、递归思路”题目它把三个 TypeScript 高级类型机制组合在一起映射类型负责逐键施加readonly修饰符自引用类型别名实现深度遍历条件类型的分发特性天然处理联合类型infer 数组分支负责把数组/元组转换为只读形态函数分支保证函数类型不被误伤。仓库的 test-cases.ts 用ExpectEqual...严格断言了两个用例深层嵌套对象 联合类型是实现正确性的权威基准template.ts 提供了初始占位。建议你在本地环境中从占位开始自行推导再用测试用例校验最后对照本指南的参考实现查漏补缺即可完整吃透递归类型工具的编写范式。赞分享示例工程【免费下载链接】type-challengesCollection of TypeScript type challenges with online judge项目地址https://gitcode.com/GitHub_Trending/ty/type-challenges点击查看免费下载相关推荐攻克TypeScript最难类型挑战DeepReadonly完全实现指南攻克TypeScript最难类型挑战DeepReadonly完全实现指南 你是否曾在TypeScript项目中遇到嵌套对象属性意外被修改的bug是否想彻底掌示例工程InsForge邮件服务终极指南SMTP配置与模板系统完全教程InsForge邮件服务终极指南SMTP配置与模板系统完全教程 InsForge作为一个全栈开源后端平台提供了强大而灵活的邮件服务系统让开发者能够轻松集成后端前端AI 应用基于声明式配置驱动的Windows系统管理引擎WinUtil架构深度解析基于声明式配置驱动的Windows系统管理引擎WinUtil架构深度解析 WinUtil作为一款开源的Windows系统管理工具采用声明式配置驱动架构和模块桌面应用运维上一篇终极指南Mac Mouse Fix鼠标侧键在macOS升级后失效的完整解决方案下一篇HEIF Utility4个实用技巧让Windows用户轻松处理iPhone照片创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考