
示例工程【免费下载链接】type-challengesCollection of TypeScript type challenges with online judge项目地址https://gitcode.com/GitHub_Trending/ty/type-challenges点击查看免费下载本篇文章聚焦于 type-challenges 仓库中的第 8 号中等难度挑战Readonly 2编号00008-medium-readonly-2完整还原题目要求、测试用例与推导过程并深入到该仓库的模板文件、测试断言与工具类型实现讲透「部分属性只读」这一泛型映射类型的标准解法。读完本文你将掌握如何为一个对象类型指定任意属性子集并使其只读、如何利用K extends keyof T做键约束与默认值回退以及Alike/Expect等测试辅助类型背后的相等性判定原理。一、题目要求两个类型参数的MyReadonly2T, K在 type-challenges 中每道题都存放在questions/目录下独立的编号文件夹中本题位于 questions/00008-medium-readonly-2其元数据 info.yml 记录标题为 Readonly 2作者 Anthony Fu难度 medium标签为readonly、object-keys。题目要求实现一个泛型MyReadonly2T, K它接收两个类型参数T和KT是目标对象类型K指定T中应被设置为只读readonly的属性集合当K未提供时行为退化为将T的所有属性变为只读等价于内置工具类型ReadonlyT。题目给出的原版示例README.md如下interface Todo { title: string description: string completed: boolean } const todo: MyReadonly2Todo, title | description { title: Hey, description: foobar, completed: false, } todo.title Hello // Error: cannot reassign a readonly property todo.description barFoo // Error: cannot reassign a readonly property todo.completed true // OK可以看到title | description被标记为只读后对这两个属性的重新赋值在编译期就会报错而未被K覆盖的completed仍然可以正常修改。这正是「对象部分属性只读」这一核心语义中文版标题见 README.zh-CN.md。二、起点模板从any到真正的映射类型你需要在 template.ts 中完成实现该文件目前只有一行占位type MyReadonly2T, K any这行any意味着无论传入什么类型结果都会退化为any显然无法满足只读约束需要通过类型运算重写。第一步理解keyof与K extends keyof TK的本质是T的键的子集因此正确写法必须加上约束type MyReadonly2T, K extends keyof T ...加上该约束后一旦调用方传入T中不存在的键例如MyReadonly2Todo1, title | invalid编译器会直接报错。这一点在测试文件 test-cases.ts 中通过ts-expect-error明确验证// ts-expect-error type error MyReadonly2Todo1, title | invalidts-expect-error断言下一行必须产生类型错误否则编译失败。换言之K必须严格属于keyof T是该题目隐含的硬性要求。第二步处理「未提供 K」的默认值题目要求K缺省时等价于ReadonlyT。在 TypeScript 中类型参数可以指定默认值因此需要同时满足「约束」与「默认值」两个条件写法是type MyReadonly2T, K extends keyof T keyof T ...K extends keyof T keyof T表达了两层含义约束extends调用时传入的K必须是T的键默认值不传第二个参数时K自动取keyof T即全部键。这样「不传 K 时全部只读」的要求就与内置ReadonlyT对齐了。第三步用映射类型 交集组装结果方案一先对K命中的键做只读映射再取交集合并剩余键type MyReadonly2T, K extends keyof T keyof T { readonly [P in K]: T[P] } OmitT, K拆解如下{ readonly [P in K]: T[P] }遍历K中的每个键产出带readonly修饰符、取值类型为T[P]的属性OmitT, K从T中剔除K命中的键保留其余属性同时保留这些属性原有的可选?与只读修饰符将两部分交叉组合得到最终类型。方案二复用内置工具类型语义更显式type MyReadonly2T, K extends keyof T keyof T ReadonlyPickT, K OmitT, KPickT, K先取出被选中的键及其原有修饰符Readonly再统一加上只读最后与OmitT, K交叉。两种写法对本题测试用例均成立。三、测试用例逐条验证可选、已只读与键约束test-cases.ts 中定义了三个接口与一个期望类型interface Todo1 { title: string description?: string // 可选属性 completed: boolean } interface Todo2 { readonly title: string // 原本就已只读 description?: string completed: boolean } interface Expected { readonly title: string readonly description?: string // 可选性被保留 completed: boolean }四条断言如下type cases [ ExpectAlikeMyReadonly2Todo1, ReadonlyTodo1, ExpectAlikeMyReadonly2Todo1, title | description, Expected, ExpectAlikeMyReadonly2Todo2, title | description, Expected, ExpectAlikeMyReadonly2Todo2, description, Expected, ]逐条解读MyReadonly2Todo1未提供 K结果必须与内置ReadonlyTodo1完全一致验证默认参数 keyof T的行为MyReadonly2Todo1, title | descriptiontitle与description变为只读且description的可选修饰符?被保留Expected中是readonly description?: stringcompleted保持可变MyReadonly2Todo2, title | descriptionTodo2.title原本就是readonly重映射后依然是只读结果与Expected一致说明实现不会破坏已有修饰符MyReadonly2Todo2, description只针对单个键title维持其原有的只读属性经由Omit保留同样匹配Expected。关于Alike为什么用也能通过相等断言测试中使用的AlikeX, Y与ExpectT来自 utils/index.d.tsexport type ExpectT extends true T export type EqualX, Y (T() T extends X ? 1 : 2) extends (T() T extends Y ? 1 : 2) ? true : false export type MergeInsertionsT T extends object ? { [K in keyof T]: MergeInsertionsT[K] } : T export type AlikeX, Y EqualMergeInsertionsX, MergeInsertionsY要点在于Equal使用「基于函数签名的可赋值性」这一严格手段判断两个类型是否等价能区分any、联合类型、只读修饰等细微差异MergeInsertions会递归地把交叉类型如{ readonly title: string } { description?: string; completed: boolean }展开合并成普通对象结构再交给Equal比较Alike将两者结合因此我们解法中用拼出的交叉类型可以与被断言为普通对象的Expected通过Alike的严格相等校验。这也解释了为什么OmitT, K必须原样保留非选中键的修饰符如果description的可选性在中间步骤丢失MergeInsertions后产物将与Expected不相等Expect会因extends true失败而报错。四、易错点与边界情况忘记默认参数只写K extends keyof T而不给 keyof T第一条断言MyReadonly2Todo1将因缺少类型实参而报错无法通过用Excludekeyof T, K代替OmitT, KOmitT, K在底层正是由PickT, Excludekeyof T, K实现二者效果一致但直接写Omit语义更清晰也避免了嵌套过深对 K 中的键使用T[P]而非泛化取值映射类型{ readonly [P in K]: T[P] }中T[P]能精确索引到原属性类型若写成any或泛化类型会破坏类型精度ts-expect-error的存在意义它强制要求K超出keyof T时必须在编译期报错这从测试层面锁死了「键约束」这条规则实现时切勿通过K extends string等宽泛约束绕过。五、挑战的定位从 Readonly 到 Deep Readonly在题目的「相关挑战」导航中见 info.yml 的related: 7, 9本题连接着两个相邻挑战构成完整的只读工具类型学习路线挑战编号名称难度核心差异7Readonlyeasy全部属性只读ReadonlyT的直接手写8本题Readonly 2medium按属性子集只读支持 K 缺省回退9Deep Readonlymedium递归处理嵌套子对象逐层加只读三者一脉相承第 7 题解决「全量只读」第 8 题在它的基础上引入「第二类型参数 键约束 默认值」的组合技巧第 9 题则进一步要求递归深入对象内部。可以这样认为第 8 题的{ readonly [P in K]: T[P] } OmitT, K正是第 9 题递归版本中「对每个键施加只读并继续下钻」的基本构件。做完本题后顺着 README.md 底部的相关挑战链接继续攻克第 9 题能自然强化映射类型与递归类型的综合运用能力。六、总结最终可在 template.ts 中提交的实现为type MyReadonly2T, K extends keyof T keyof T { readonly [P in K]: T[P] } OmitT, K它用四步走完整回答了题目K extends keyof T约束键必须是T的合法键 keyof T提供默认值缺省时退化为ReadonlyT{ readonly [P in K]: T[P] }只对选中的键施加只读 OmitT, K合并剩余键并保留其原有可选/只读修饰符。对照 test-cases.ts 的四条断言与一条ts-expect-error该实现覆盖了「全量只读、部分只读、保留可选性、兼容既有 readonly、非法键报错」全部场景。理解这一题的推导链你就掌握了 TypeScript 泛型工具类型中最常用的一组核心技巧映射类型mapped type、键约束extends keyof、默认类型参数 default与类型交叉的组合。赞分享示例工程【免费下载链接】type-challengesCollection of TypeScript type challenges with online judge项目地址https://gitcode.com/GitHub_Trending/ty/type-challenges点击查看免费下载相关推荐es-toolkit/fp 数据末位 without在 pipe 管线中原生剔除指定值es toolkit/fp 数据末位 without在 pipe 管线中原生剔除指定值 without FP 变体是 es toolkit 函数式编程模块示例工程type-challenges 第 8 题 MyReadonly2实现「对象部分属性只读」的进阶泛型type challenges 第 8 题 MyReadonly2实现「对象部分属性只读」的进阶泛型 本篇文章以 questions/00008 medium示例工程SANA 安装指南从零搭建环境到 Diffusers 快速推理实战SANA 安装指南从零搭建环境到 Diffusers 快速推理实战 本指南是 SANAEfficient High Resolution Image Syn示例工程上一篇CocoaLumberjack Swift Concurrency支持Async/Await日志API下一篇EMQX ClickHouse Bridge 数据集成实战配置、SQL 模板与实现原理创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考