Append to Object
Append to Object
题目
实现一个向接口添加新字段的类型。该类型接受三个参数,输出结果应为包含新字段的对象类型。
type Test = { id: '1' }
type Result = AppendToObject<Test, 'value', 4> // 期望结果:{ id: '1', value: 4 }解答
type AppendToObject<T, U extends string, V> = {
[K in keyof T | U]: K extends keyof T ? T[K] : V
}核心思路:遍历已有键与新键的联合,再用条件类型决定每个属性的值类型。
逐步拆解
第一步 — 合并键集:keyof T | U
keyof T 取出 T 的所有已有键,U 是要新增的键,两者取联合,映射类型就能覆盖所有属性——原有的和新增的都不会漏掉。
第二步 — 决定每个键的值类型
K extends keyof T ? T[K] : V- 如果
K已经是T的键 → 保留原始值类型T[K] - 否则(即
K === U)→ 这是新字段,赋予类型V
结果: 一个扁平的对象类型,包含所有原有属性,加上新的 U: V 条目。
为什么不直接用 T & { [P in U]: V }?
交叉类型的写法也能工作,但结果是交叉类型而非扁平对象:
type AppendToObject<T, U extends string, V> = T & { [P in U]: V }
// 结果类型:{ id: '1' } & { value: 4 }TypeScript 在结构上会把它们视为等价,但在 IDE 的类型提示里显示的是交叉类型,而不是一个干净的合并对象。映射类型方案产生的是单一、扁平的对象类型,更直观、更易读。
深入理解
约束 U extends string 的意义
TypeScript 中对象的键可以是 string、number 或 symbol。这里用 U extends string 约束新键必须是字符串字面量类型。没有这个约束,TypeScript 会报错,因为 keyof T | U 要求合法的键类型。
实际上,这个约束还能让 TypeScript 把 U 收窄到具体的字符串字面量(例如 'value'),这才能让 AppendToObject<Test, 'value', 4> 解析为 { id: '1'; value: 4 },而不是 { id: '1'; [x: string]: 4 }。
如果新键已存在会怎样?
如果 U 已经是 T 的键,条件 K extends keyof T ? T[K] : V 永远走第一个分支,保留原始类型——新值 V 不会覆盖旧值。如果想让新值优先(类似 Merge 的语义),调换条件顺序即可:
type AppendOrOverwrite<T, U extends string, V> = {
[K in keyof T | U]: K extends U ? V : K extends keyof T ? T[K] : never
}与 Merge 的关系
AppendToObject 本质上就是一个特殊化的 Merge,第二个"类型"只是一个只有一个属性的对象 { [U]: V }:
type AppendToObject<T, U extends string, V> = Merge<T, { [P in U]: V }>先理解了 Merge,AppendToObject 就变得极其简单——本质是同一个模式。
同态映射类型 vs 非同态映射类型
这里的映射类型是非同态的:它不是直接对 keyof T 映射,所以 T 上的修饰符如 readonly 和 ? 不会自动保留。
type Source = { readonly id: string; name?: string }
type Result = AppendToObject<Source, 'age', number>
// { id: string; name: string | undefined; age: number }
// ^ readonly 和 ? 都丢失了如果需要保留修饰符,可以把映射类型拆成两部分取交叉:
type AppendToObject<T, U extends string, V> =
{ [K in keyof T]: T[K] } & { [P in U]: V }对于大多数使用场景(包括这道题),简单版本已经足够。
核心要点
keyof T | U在映射类型中是向已有对象类型"新增一个键"的最简洁方式- 对键的条件类型(
K extends keyof T ? T[K] : V)优雅地区分了已有属性和新属性 - 交叉类型简写(
T & { [P in U]: V })等价但产生非扁平类型 U extends string是必要约束,让U成为合法的映射类型键AppendToObject是Merge的退化情形——第二个操作数仅为单属性对象
