
示例工程【免费下载链接】type-challengesCollection of TypeScript type challenges with online judge项目地址https://gitcode.com/GitHub_Trending/ty/type-challenges点击查看免费下载本篇指南以 questions/00009-medium-deep-readonly/README.pt-BR.md英文版见 README.md为骨架展开它要求实现一个泛型DeepReadonlyT把对象的所有属性及其子对象递归地标记为只读。在 type-challenges 仓库中本题是中等难度medium标签为readonly、object-keys、deep与第 7 题 Readonly、第 8 题 Readonly 2 构成由浅入深的递进序列。读完本文你将掌握递归映射类型、函数/数组/元组的特判处理以及联合类型分发在条件类型中的行为并能通过仓库内的测试用例在本地验证自己的实现。挑战背景从单层只读到深层只读本题由 Anthony Fu 创作仓库通过info.yml记录了题目的元数据难度为medium标签为readonly、object-keys、deep并声明关联题目为第 7、8 题见 questions/00009-medium-deep-readonly/info.yml。要理解 Deep Readonly 的价值先看它所在的递进序列第 7 题 Readonly简单不用内置ReadonlyT自行实现将所有属性置为只读的单层版本第 8 题 Readonly 2中等引入第二个类型参数K只把K指定的属性设为只读未提供K时退化为ReadonlyT第 9 题 Deep Readonly中等不再停留在顶层而是要求递归处理所有子对象这是前两题的自然延伸。内置的ReadonlyT只处理一层Readonly{ x: { a: 1 } }得到{ readonly x: { a: 1 } }内层的a依然可写。Deep Readonly 的价值就在于把只读约束传播到对象图的每一个节点这在配置对象、嵌套状态等场景中非常实用——一旦编译通过运行时就不再可能出现深层属性被误改。题目目标DeepReadonly 的定义与假设原文档README.pt-BR.md给出了清晰的目标描述实现泛型DeepReadonlyT将对象的每一个参数及其子对象递归地设为只读torne todos os parâmetros de um objeto - e seus subobjetos recursivamente - somente leitura。同时文档明确给出两条边界假设本题只处理对象数组、函数、类等不需要考虑但鼓励挑战者自行覆盖尽可能多的不同类型案例you can still challenge yourself by covering as many different cases as possible。这两条假设非常重要它们决定了最小可通关实现与完整健壮实现之间的差距——前者只需递归映射对象而仓库的测试用例见下文实际上还覆盖了函数、数组、元组与联合类型因此想真正通过判题就必须处理这些超出最小假设的情况。原文档给出的核心示例完整保留如下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可见x本身变为只读同时x内部的a、b也变为只读y作为叶子属性同样只读。这就是递归二字的全部含义。测试用例揭示的真实判题约束题目的起点模板极其简单——template.ts 中只有一行占位type DeepReadonlyT any真正决定实现正确性的是 test-cases.ts 中的两组用例。它们使用type-challenges/utils提供的Equal与Expect做严格的结构相等校验type cases [ ExpectEqualDeepReadonlyX1, Expected1, ExpectEqualDeepReadonlyX2, Expected2, ]其中Equal的实现位于 utils/index.d.ts利用函数参数位置的逆变性精确判断两个类型是否完全一致含 readonly 修饰符差异因此差不多对是过不了判题的。用例一函数、深层嵌套对象与数组元组X1是一个高度嵌套的复合结构test-cases.tstype X1 { a: () 22 b: string c: { d: boolean e: { g: { h: { i: true j: string } k: hello } l: [ hi, { m: [hey] }, ] } } }对应的期望结果Expected1test-cases.ts暴露了三个关键事实函数属性原样保留a: () 22变成readonly a: () 22函数本身不被拆解。从源码结构可以推断实现必须先用条件类型把函数类型原样返回否则keyof (() 22)会把函数当作普通对象去映射产生length、name之类的伪属性数组与元组也要递归只读l: [ hi, { m: [hey] } ]变成readonly l: readonly [ hi, { readonly m: readonly [hey] } ]——元组本身加了readonly元组里的对象成员也递归只读数组[hey]同样变为readonly [hey]深度无上限c - e - g - h - i五层嵌套全部只读证明实现必须是真正的递归而非有限深度展开。用例二联合类型的分发X2是联合类型{ a: string } | { b: number }test-cases.ts期望结果是{ readonly a: string } | { readonly b: number }。这里利用了 TypeScript 条件类型的一个重要机制当裸类型参数未被任何结构包裹遇到条件类型时联合类型会被分发到每个成员上分别求值。因此DeepReadonly{ a: string } | { b: number }等价于分别对{ a: string }和{ b: number }应用DeepReadonly再把结果重新组成联合——这要求实现中T extends ...的判断必须作用于裸的T而不能包裹在T[]、keyof T之类结构中否则会破坏分发。实现思路从最小解法到健壮解法第一步递归映射对象最朴素的想法是利用映射类型mapped type加递归type DeepReadonlyT { readonly [K in keyof T]: DeepReadonlyT[K] }对纯对象而言它已经满足要求每一层都递归调用自身且每一层的键都加上readonly。但它有两个致命缺陷函数会被错误展开原始类型如string的keyof为空映射后虽仍等价但语义可疑。测试用例直接否决了这种写法。第二步排除函数对照Expected1中readonly a: () 22给递归分支加一道闸门type DeepReadonlyT T extends (...args: any[]) any ? T : { readonly [K in keyof T]: DeepReadonlyT[K] }T extends (...args: any[]) any能匹配一切可调用类型普通函数、箭头函数、方法命中后原样返回。此时T是裸类型参数联合类型X2会在两个分支间正确分发天然满足Expected2。第三步让数组与元组同样只读关键在于映射类型对数组/元组的处理同态映射homomorphic mapped type会保留数组与元组的结构形态。对元组T执行{ readonly [K in keyof T]: ... }K遍历的是数字索引0 | 1 | ...结果仍是元组只是每个元素被递归处理并整体加上readonly对数组同理得到readonly数组。这正是Expected1中readonly l: readonly [hi, {...}]与readonly m: readonly [hey]的来源——不需要显式特判数组第二步的映射分支已经覆盖。最终的最小可通关实现只需这两行核心逻辑type DeepReadonlyT T extends (...args: any[]) any ? T : { readonly [K in keyof T]: DeepReadonlyT[K] }第四步自选挑战覆盖更多类型原文档鼓励覆盖尽可能多的不同案例。例如要保护Date、RegExp这类内部状态不应被展开的对象或Map、Set等集合容器可以在闸门处扩展排除列表type Primitive Date | RegExp | Mapany, any | Setany type DeepReadonlyT T extends (...args: any[]) any | Primitive ? T : { readonly [K in keyof T]: DeepReadonlyT[K] }注意若对Map、Set想做到键/值也只读的极致深度可以进一步用ReadonlyMap、ReadonlySet包裹其内部类型这一步已超出原题要求属于可选的自我挑战范畴。边界情况速查输入类型行为说明字面量/原始类型1、hi、true原样返回keyof为空映射不产生任何属性函数() 22原样返回由T extends (...args: any[]) any闸门拦截数组[hey]readonly [hey]同态映射保留数组形态并整体只读元组[hi, { m: [...] }]readonly [hi, { readonly m: readonly [...] }]逐元素递归索引键保留联合类型{ a: string } \| { b: number }每个成员独立只读后重组联合裸类型参数触发条件类型分发深层嵌套对象全层级只读递归无上限本地运行与验证在仓库中验证实现的方式很直接每个题目目录都包含一个template.ts起始为type DeepReadonlyT any和对应的test-cases.ts。把上面第二步的实现填入template.ts后用 TypeScript 编译器对测试文件做类型检查即可。仓库根目录使用 pnpm workspace见 package.jsontype-challenges/utils是 workspace 内部包提供Equal、Expect等断言工具。典型验证流程# 在仓库根目录安装依赖使 type-challenges/utils 可用 pnpm install # 对第 9 题的测试用例做类型检查不产出 JS npx tsc --noEmit questions/00009-medium-deep-readonly/test-cases.ts若DeepReadonly实现正确以上命令应零报错若实现有误例如函数被拆解、元组未只读、联合未分发ExpectEqual...会在对应行报出类型不匹配错误。你也可以借助编辑器的 TypeScript 语言服务将type Todo DeepReadonlyX悬停展开直接观察每一层的readonly是否如Expected所示。仓库还在 guides/recursive.md 预留了递归类型的专题指南目录当前为占位内容配合本类题目学习递归类型是官方推荐的路径。总结Deep Readonly 是 TypeScript 类型编程中递归 映射 分发三者交汇的经典题目。从原文档看它的核心诉求只是对象及其子对象递归只读但从仓库的 test-cases.ts 看一个能通关的实现还必须回答三个额外问题函数怎么办原样返回、数组/元组怎么办同态映射天然只读、联合类型怎么办裸类型参数自动分发。最终解法浓缩为两行type DeepReadonlyT T extends (...args: any[]) any ? T : { readonly [K in keyof T]: DeepReadonlyT[K] }掌握它你就同时掌握了递归条件类型、映射类型的同态特性与条件类型分发三大基本功——它们会反复出现在 type-challenges 后续的 harder 题目中。赞分享示例工程【免费下载链接】type-challengesCollection of TypeScript type challenges with online judge项目地址https://gitcode.com/GitHub_Trending/ty/type-challenges点击查看免费下载相关推荐Electric 1.0 GA 解读基于 Postgres 的同步引擎如何做到稳定 API、生产就绪与百万级并发Electric 1.0 GA 解读基于 Postgres 的同步引擎如何做到稳定 API、生产就绪与百万级并发 Electric当前仓库 GitHub_T示例工程Kubernetes Goat 场景一实战从 .git 暴露到 TruffleHog 检测代码库中的敏感密钥Kubernetes Goat 场景一实战从 .git 暴露到 TruffleHog 检测代码库中的敏感密钥 本文围绕 Kubernetes Goat 的第一示例工程SANA 安装指南从零搭建环境到 Diffusers 快速推理实战SANA 安装指南从零搭建环境到 Diffusers 快速推理实战 本指南是 SANAEfficient High Resolution Image Syn示例工程上一篇macOS自动点击器解放双手的效率工具完全指南下一篇UI-TARS桌面版3分钟快速上手的智能桌面助手完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考