在 TypeScript 类型体操中,元组类型和对象映射类型经常一起出现。假设有一个字符串元组 ['id', 'name', 'age'],我们想把它转换为类似 Map<'id' | 'name' | 'age', string> 这样的键值对类型,或者转换为 { id: 'id'; name: 'name'; age: 'age' } 的对象形式。第一步不是急着写条件类型,而是先搞清楚元组类型在类型系统里到底长什么样,以及如何把它的所有元素提取成一个独立的联合类型。这是后续进行键值对转换的基础,也是很多复杂类型工具能够复用的一段小程序。

一、从元组中提取所有元素类型
元组类型可以看作长度固定、元素类型可以各不相同的数组类型。TypeScript 为数组和元组提供了 number 索引访问,也就是说,对于任意元组类型 T,使用 T[number] 可以得到所有元素位置上的类型联合。例如 ['id', 'name'] 经过索引访问后得到 'id' | 'name'。这个机制能够成立,是因为元组本身是数组的子类型,而数组的索引签名是 number,所以 TypeScript 会把每个成员类型收集起来做联合。
这个步骤看起来简单,但它是后续所有转换的起点。如果元组被声明为 readonly ['id', 'name'],T[number] 依然可以正常工作,并不会因为只读修饰符而失效。唯一需要注意的是空元组 [],它的 T[number] 会得到 never,因为没有任何元素可以提取。因此后面写工具类型时要考虑 never 在映射类型中的表现,否则可能生成意料之外的空对象。
type Tuple = ['id', 'name', 'age']; type Element = Tuple[number]; // 'id' | 'name' | 'age' type ReadonlyTuple = readonly ['id', 'name']; type ReadonlyElement = ReadonlyTuple[number]; // 'id' | 'name' type EmptyTuple = []; type EmptyElement = EmptyTuple[number]; // never
二、用映射类型生成对象键值对
拿到 T[number] 之后,最直接的目标就是生成一个对象映射类型。TypeScript 的映射类型允许我们用 in 关键字遍历一个联合类型,并把每个成员作为新对象的键。对于字符串元组,可以写出下面这个工具类型:
type TupleToRecord<T extends readonly PropertyKey[]> = {
[K in T[number]]: K;
};
type Result = TupleToRecord<['id', 'name']>;
// Result = { id: 'id'; name: 'name' }
这里泛型约束 T extends readonly PropertyKey[] 用来限制元组中的每个元素都必须是合法的键类型,也就是 string | number | symbol。如果不加这个约束,当元组里出现对象或数组元素时,映射类型会因为键类型不合法而直接报错。值类型 K 可以根据需求改成任意其他类型,比如让键对应的值固定为 boolean,或者用条件类型从某个接口里取出对应属性的类型。
这种对象形式的键值对在实际项目中非常实用。例如后端返回的字段名元组,需要在前端拼装成默认值对象,就可以用 TupleToRecord 生成每个字段默认值为字段名本身的结构,再配合运行时逻辑处理。它的优点是可读性强,生成的类型在编辑器中展开后就是一目了然的对象形状,比直接操作 Map 泛型更符合大多数业务场景。
三、生成真正的 Map 类型与严格一一对应
如果确实要把类型声明为 JavaScript 内置的 Map<K, V>,情况会稍微复杂一点。因为 Map 的泛型参数只接受两个联合类型,分别代表所有键的集合和所有值的集合。假设元组里的元素既可以作为键,又可以作为值,可以写出下面这个工具类型:
type TupleToMapKeys<T extends readonly PropertyKey[]> = T[number]; type TupleToMapValues<T extends readonly PropertyKey[]> = T[number]; type MapType<T extends readonly PropertyKey[]> = Map<TupleToMapKeys<T>, TupleToMapValues<T>>; type ResultMap = MapType<['id', 'name']>; // ResultMap = Map<'id' | 'name', 'id' | 'name'>
不过这种写法存在一个缺陷:它只能表达键和值来自同一个联合类型,无法保证某个键一定对应某个值。例如 Map<'id' | 'name', 'id' | 'name'> 在类型层面允许 set('id', 'name'),这显然不符合从元组元素一一对应转换的直觉。如果业务上需要严格保留每个键和它自身的对应关系,应该先生成元组对联合,或者直接生成对象映射,再根据对象映射推导 Map 类型。
要构造严格一一对应的元组对,可以借助条件类型的联合分配特性。下面的工具类型会把 T[number] 中的每个元素拆开,转换成 [K, K] 形式的元组对联合:
type TupleToEntries<T extends readonly PropertyKey[]> = T[number] extends infer K
? K extends PropertyKey
? [K, K]
: never
: never;
type Entries = TupleToEntries<['id', 'name']>;
// Entries = ['id', 'id'] | ['name', 'name']
得到 Entries 后,就可以继续配合其他工具类型,例如把元组对联合转换成对象映射,或者基于它实现更细粒度的类型校验。不过在日常开发中,如果只是想描述键值对结构,直接用对象映射类型通常比使用 Map 泛型更简洁,因为在 TypeScript 的类型系统里,对象字面量类型比 Map 的泛型参数更容易被后续的类型判断和属性访问所利用。
四、处理可选元素、重复键与混合类型元组
元组类型有时会带有可选元素,例如 ['id', 'name'?]。此时 T[number] 会得到 'id' | 'name' | undefined,如果直接把 undefined 放进映射类型,会触发报错,因为 undefined 不是合法的键类型。解决方式是先用 Exclude 把 undefined 从联合类型中过滤掉,再做后续映射。
type CleanKeys<T extends readonly unknown[]> = Exclude<T[number], undefined> extends infer K
? K extends PropertyKey
? K
: never
: never;
type TupleToRecordSafe<T extends readonly unknown[]> = {
[K in CleanKeys<T>]: K;
};
type SafeResult = TupleToRecordSafe<['id', 'name'?]>;
// SafeResult = { id: 'id'; name: 'name' }
这个 CleanKeys 工具类型一共做了两件事:先把 undefined 排除掉,再确认剩余的每个成员都满足 PropertyKey 约束。由于条件类型对联合类型有分配能力,extends infer K 会把 'id' | 'name' 逐一拆开判断,最终再合并成干净的键联合。对于重复键,对象映射类型会自动合并,后面的定义会覆盖前面的定义,因此如果元组是 ['id', 'id'],生成的映射类型仍然是 { id: 'id' },不会出现两个 id 键。
混合类型元组也值得留意。比如元组 ['id', 1] 提取出的联合类型是 'id' | 1,其中 1 是数字字面量类型,同样可以作为对象键。只要在映射类型之前用 PropertyKey 约束或过滤,TypeScript 就能正确处理。理解了这些边界情况之后,再把元组转成 Map 键值对类型就不再是难题,而且可以根据业务需要自由选择对象映射、联合键值对或者严格元组对等不同表达方式。
TypeScript类型体操元组转Map修改时间:2026-09-21 09:35:02