# 基操-类型转换大集合
TS 体操里常常会碰到「手上拿着类型 A,题目要我输出类型 B」。这种时候心里要先有个清单:不同形态的类型之间,到底有哪些通路可走。这篇就把常用的几条通路捋一遍,后面的题解只要写「参考 类型转换大集合」就行,不再每篇重讲。
# 几种基本"形态"
先约定下后文会用到的概念。TS 里参与体操的类型大致分四类:
- 字面量 / 基础类型:
'a',1,true,string,number。 - 联合类型:
'a' | 'b',1 | 2。 - 元组 / 数组:
[1, 2, 3],number[]。 - 对象 / 接口:
{ a: 1; b: 2 }。
大多数转换题目就是要把上面四类互相变换,或者改造其中一种的内部结构。
# 对象 ↔ 对象
最基础也是最高频的形态,核心是 mapped type:
type Copy<T> = {
[P in keyof T]: T[P];
};
配合 as 可以重命名 key,或用 never 过滤 key:
// 前缀加 'get_'
type Prefixed<T> = {
[P in keyof T as `get_${string & P}`]: T[P];
};
// 把值为 string 的字段去掉
type DropStringFields<T> = {
[P in keyof T as T[P] extends string ? never : P]: T[P];
};
[P in keyof T as ...] 是对象遍历时最锋利的改键手段,详细可看 对象遍历的 as 和索引访问。
# 对象 → 联合
三种最常见的做法:
type O = { a: 1; b: 2; c: 3 };
// 1. 取所有 key 组成联合
type Keys = keyof O; // 'a' | 'b' | 'c'
// 2. 取所有 value 组成联合
type Values = O[keyof O]; // 1 | 2 | 3
// 3. 取 [key, value] 对的联合(Entries)
type Entries = { [K in keyof O]: [K, O[K]] }[keyof O];
// ['a', 1] | ['b', 2] | ['c', 3]
第 3 条的套路在很多"类对象转列表"的题里都能看到:先用 mapped type 把每个字段改成 [K, V],再用 [keyof O] 索引访问,一步把所有字段的新值"拉平"成联合。
# 对象 → 元组
对象本身是无序的,没法直接转成元组,通常要经过联合再拼:
// 先转联合,再递归把联合拆成元组
type UnionToTuple<U> = /* 见 hard/730 */;
type KeysTuple<T> = UnionToTuple<keyof T>;
这条路径涉及「联合转元组」,属于 hard 级别技巧,详见 hard/730-联合转元组。
# 联合 → 对象
反过来,拿到一堆 key 的联合,想拼成对象,用 mapped type 配字面量 as:
type ToObject<U extends string> = {
[K in U]: K;
};
type R = ToObject<'a' | 'b'>; // { a: 'a'; b: 'b' }
联合作为 in 的右侧时会自动把每一支作为新对象的 key,这也是 Record 的实现原理:
type MyRecord<K extends keyof any, V> = {
[P in K]: V;
};
# 联合 ↔ 交叉
联合转交叉靠函数参数逆变:
type UnionToIntersection<U> = (U extends any ? (x: U) => any : never) extends (
x: infer I,
) => any
? I
: never;
type R = UnionToIntersection<{ a: 1 } | { b: 2 }>; // { a: 1 } & { b: 2 }
背后是分发 + 逆变位置上的类型推断合并,见 逆变顺变协变。
交叉转联合一般不直接做,通常是靠 mapped type 把交叉的 key 遍历一遍再取出来。
# 元组 ↔ 联合
// 元组 -> 联合
type T = [1, 2, 3];
type U = T[number]; // 1 | 2 | 3
// 联合 -> 元组(依赖 hard/730 中的逆变推断)
T[number] 是元组转联合最简单直接的手段。几乎所有需要"遍历元组值"的题(如 Includes、AnyOf)都用它。
# 元组 ↔ 元组
元组的改造基本靠 infer 拆首尾 + 递归重组:
// 取首
type First<T extends any[]> = T extends [infer F, ...any] ? F : never;
// 取尾
type Last<T extends any[]> = T extends [...any, infer L] ? L : never;
// 每一项套一层
type Wrap<T extends any[]> = {
[K in keyof T]: [T[K]];
};
值得单独记住的是"用对象 mapped type 的语法改造元组":[K in keyof T] 对元组而言 K 是索引 ('0' | '1' | ...),但 TS 会保留元组形态,这是元组遍历里最省力的写法,详见 元组遍历的黑科技。
# 字符串 ↔ 联合
字符串和联合的互转主要靠字符串模板:
// 字符串逐字符转联合:需要递归
type StrToUnion<S> = S extends `${infer F}${infer R}`
? F | StrToUnion<R>
: never;
type U = StrToUnion<'abc'>; // 'a' | 'b' | 'c'
// 联合转字符串(要求联合的每一支都是 string literal)
type U2S<U extends string> = U extends U ? `${U}` : never;
分隔符拆分更常用:
type Split<
S extends string,
D extends string,
> = S extends `${infer H}${D}${infer T}` ? [H, ...Split<T, D>] : [S];
type R = Split<'a,b,c', ','>; // ['a', 'b', 'c']
# 字符串 ↔ 数字
TS 4.x 以后字符串到数字可以直接写 ${number} 模板,但想拿到具体数值还得绕一圈:
// 字符串 -> 数字(利用长度)
type StrLen<
S extends string,
A extends any[] = [],
> = S extends `${string}${infer R}` ? StrLen<R, [...A, 1]> : A['length'];
// 数字 -> 字符串
type NumToStr<N extends number> = `${N}`;
数字本身参与运算都要借助「元组长度」,详细见 加减乘除。
# 字符串 ↔ 字符串
这类纯模板字符串操作属于体操的重灾区,常见模式:
// 大小写:内置 Uppercase / Lowercase / Capitalize / Uncapitalize
type A = Uppercase<'abc'>; // 'ABC'
// 遍历每一个字符并改造
type Map<S, F extends string> = S extends `${infer H}${infer T}`
? `${F}${H}${Map<T, F>}`
: '';
// 替换首个 / 全部
type Replace<
S,
From extends string,
To extends string,
> = S extends `${infer H}${From}${infer R}` ? `${H}${To}${R}` : S;
# 速查表
| 起点 | 终点 | 主要手段 |
|---|---|---|
| 对象 → 对象 | 改键/改值 | mapped type + as |
| 对象 → 联合 | key | keyof T |
| 对象 → 联合 | value | T[keyof T] |
| 对象 → 联合 | entry | { [K in keyof T]: [K, T[K]] }[keyof T] |
| 联合 → 对象 | mapped type [K in U] | |
| 联合 → 交叉 | 逆变 (U extends any ? (x: U) => ... ) extends (x: infer I) => ... | |
| 联合 → 元组 | hard/730 | |
| 元组 → 联合 | T[number] | |
| 元组 → 元组 | infer 拆首尾 / [K in keyof T] | |
| 字符串 → 联合 | 按字符 | 模板 + 递归 ${infer F}${infer R} |
| 字符串 → 元组 | 按分隔符 | Split 模板递归 |
| 字符串 ↔ 数字 | ${number}, 元组长度计数 |
# 被谁用到
几乎所有 medium / hard 题都会用到这里面的一两条。高频引用点:
- 对象改键:medium/4179-实现 Flip, medium/2595-实现 PickByType。
- 对象 → entries:hard/2949-ObjectFromEntries。
- 联合 → 交叉:hard/55-UnionToIntersection。
- 联合 → 元组:hard/730-联合转元组。
- 字符串 ↔ 联合:medium/531-字符转联合, medium/298-计算字符的长度。
- 元组遍历:medium/10-元组转联合, medium/3192-实现 Reverse。
碰到"拿到类型 A 要变 B"时,先在脑子里走一遍上面这张表,多半就知道从哪条通路下刀了。