TypeScript类型体操

本文面向已经会写基础 TypeScript 的前端/全栈开发者,系统梳理类型体操常见的 100 个知识点。可直接复制到 .ts 文件中验证。

一、类型体操是什么

类型体操不是为了炫技,而是利用 TypeScript 的类型系统在编译期完成约束、推导、转换和校验。它常见于工具类型、组件库 Props 推导、接口返回值建模、路由参数解析、表单 schema 推导、状态管理类型收窄等场景。

flowchart TD A[输入类型] --> B[条件判断] B --> C[类型推导] C --> D[类型转换] D --> E[输出新类型]

二、基础认知:1-15

1. 类型和值是两个世界

TypeScript 的类型只存在于编译期,运行时不会保留大多数类型信息。

ts 复制代码
type User = { name: string };
const user: User = { name: 'Tom' };

User 是类型,user 是值。类型体操操作的是类型世界。

2. type 适合表达复杂类型

type 可以定义联合类型、交叉类型、条件类型、映射类型等。

ts 复制代码
type ID = string | number;
type Result<T> = T extends Error ? false : true;

类型体操通常以 type 为主要载体。

3. interface 适合对象结构扩展

interface 支持声明合并和继承,更适合公开 API 对象形状。

ts 复制代码
interface User {
  name: string;
}

interface User {
  age: number;
}

合并后 User 同时包含 nameage

4. 字面量类型是精确类型

ts 复制代码
type Method = 'GET' | 'POST';

字面量类型比 string 更精确,是类型体操判断分支的重要材料。

5. 联合类型表示"或"

ts 复制代码
type Status = 'loading' | 'success' | 'error';

类型体操经常对联合类型逐项分发处理。

6. 交叉类型表示"合并约束"

ts 复制代码
type A = { name: string };
type B = { age: number };
type C = A & B;

C 同时需要满足 AB

7. never 表示不可能

ts 复制代码
type Impossible = string & number;

Impossiblenever。它常用于过滤联合类型。

8. unknown 是安全的顶层类型

任何值都能赋给 unknown,但使用前必须收窄。

ts 复制代码
let value: unknown;
// value.toFixed(); // 报错

类型体操中 unknown 常用于泛型约束的安全兜底。

9. any 会放弃类型检查

ts 复制代码
type A = any extends string ? 1 : 2;

any 会污染类型推导,复杂工具类型应尽量少用。

10. voidundefined 不完全等价

函数返回 void 表示调用者不应该依赖返回值。

ts 复制代码
type Fn = () => void;

在工具类型里处理函数返回值时需要区分 voidundefinednever

11. object 不等于普通对象字面量

object 表示非原始类型,不包含 stringnumberboolean 等。

ts 复制代码
type IsObject<T> = T extends object ? true : false;

函数、数组也满足 object

12. keyof 获取对象键名联合

ts 复制代码
type User = { name: string; age: number };
type Keys = keyof User; // 'name' | 'age'

这是对象类型遍历的入口。

13. 索引访问类型读取属性类型

ts 复制代码
type User = { name: string; age: number };
type Name = User['name'];

也可以读取多个属性:User[keyof User] 得到属性值联合。

14. typeof 从值反推类型

ts 复制代码
const config = { mode: 'dark', size: 12 };
type Config = typeof config;

注意普通对象字面量会发生类型拓宽。

15. as const 锁定字面量

ts 复制代码
const routes = ['home', 'about'] as const;
type Route = typeof routes[number]; // 'home' | 'about'

as const 是从运行时配置生成精确类型的常用起点。

三、泛型基础:16-30

16. 泛型是类型层面的参数

ts 复制代码
type Box<T> = { value: T };

T 像函数参数一样,可以把外部类型传入内部。

17. 泛型约束使用 extends

ts 复制代码
type Prop<T extends object, K extends keyof T> = T[K];

约束能保证后续类型操作合法。

18. 泛型默认值降低使用成本

ts 复制代码
type ApiResponse<T = unknown> = {
  data: T;
  code: number;
};

默认值适合工具类型的可选配置。

19. 泛型可以依赖前一个泛型

ts 复制代码
type PickValue<T, K extends keyof T = keyof T> = T[K];

后面的泛型可以使用前面泛型推导出的结果。

20. 泛型推导来自调用位置

ts 复制代码
function identity<T>(value: T): T {
  return value;
}

const result = identity('hello'); // string

函数泛型一般由参数自动推导。

21. 泛型推导会尽量选择宽类型

ts 复制代码
function wrap<T>(value: T) {
  return { value };
}

const a = wrap('x'); // { value: string }

如果需要字面量类型,可结合 as const 或 const 泛型。

22. const 泛型保留字面量信息

ts 复制代码
function tuple<const T extends readonly unknown[]>(value: T): T {
  return value;
}

const t = tuple(['a', 'b']); // readonly ['a', 'b']

这是 TS 5.x 中非常实用的推导增强。

23. 泛型约束不等于泛型结果

ts 复制代码
type GetLength<T extends { length: number }> = T['length'];

T 只是被限制有 length,但仍保留传入类型本身的信息。

24. 多泛型之间可以建立关系

ts 复制代码
type Assign<T extends object, U extends object> = T & U;

复杂类型体操本质是建立多个类型参数之间的推导关系。

25. 泛型可以用于类型函数

ts 复制代码
type ToArray<T> = T[];

类型体操里的 type 可以理解为"类型函数"。

26. 泛型分发依赖裸类型参数

ts 复制代码
type ToArray<T> = T extends unknown ? T[] : never;
type R = ToArray<string | number>; // string[] | number[]

T 出现在 extends 左侧时会触发联合类型分发。

27. 包裹泛型可阻止分发

ts 复制代码
type ToArrayNoDistribute<T> = [T] extends [unknown] ? T[] : never;
type R = ToArrayNoDistribute<string | number>; // (string | number)[]

这是控制条件类型行为的关键技巧。

28. 泛型也能用于类和接口

ts 复制代码
interface Store<State> {
  getState(): State;
}

类型体操不局限于 type,只是 type 更灵活。

29. 泛型需要避免过度抽象

如果某个类型只用一次,且没有复用价值,直接写具体类型更清晰。

ts 复制代码
type UserName = string;

这种别名如果没有语义价值,可能只是噪音。

30. 泛型命名要表达含义

常见约定:T 表示通用类型,K 表示键,V 表示值,R 表示返回值。

ts 复制代码
type ValueOf<T> = T[keyof T];

复杂场景可以使用 SourceTargetOptions 等更清晰的名字。

四、条件类型与推导:31-45

flowchart TD A[T extends U] --> B[满足约束] A --> C[不满足约束] B --> D[返回 TrueBranch] C --> E[返回 FalseBranch]

31. 条件类型是类型层面的 if

ts 复制代码
type IsString<T> = T extends string ? true : false;

它根据类型兼容关系返回不同结果。

32. extends 判断的是可赋值性

ts 复制代码
type A = 'x' extends string ? true : false; // true
type B = string extends 'x' ? true : false; // false

不是"相等",而是"是否能赋值给"。

33. 条件类型可以嵌套

ts 复制代码
type TypeName<T> = T extends string
  ? 'string'
  : T extends number
    ? 'number'
    : 'other';

嵌套过深会降低可读性,必要时拆分。

34. infer 用于在条件类型中声明待推导变量

ts 复制代码
type Return<T> = T extends (...args: any[]) => infer R ? R : never;

R 由函数返回值位置推导得出。

35. infer 可以推导数组元素

ts 复制代码
type Element<T> = T extends Array<infer Item> ? Item : never;

Element<string[]> 得到 string

36. infer 可以推导 Promise 结果

ts 复制代码
type UnwrapPromise<T> = T extends Promise<infer R> ? R : T;

实际项目中常用于 API 返回值处理。

37. infer 可以推导函数参数

ts 复制代码
type FirstArg<T> = T extends (arg: infer A, ...rest: any[]) => any ? A : never;

可以提取回调函数的参数类型。

38. 多个 infer 可以同时存在

ts 复制代码
type FnParts<T> = T extends (...args: infer P) => infer R ? [P, R] : never;

同时获得参数元组和返回值。

39. infer 可以加约束

ts 复制代码
type FirstString<T> = T extends [infer F extends string, ...unknown[]] ? F : never;

推导变量本身也能被限制。

40. 条件类型能过滤联合类型

ts 复制代码
type OnlyString<T> = T extends string ? T : never;
type R = OnlyString<string | number | boolean>; // string

never 在联合类型中会被自动消除。

41. 条件类型能排除类型

ts 复制代码
type MyExclude<T, U> = T extends U ? never : T;

这就是内置 Exclude 的核心思想。

42. 条件类型能提取类型

ts 复制代码
type MyExtract<T, U> = T extends U ? T : never;

这就是内置 Extract 的核心思想。

43. 判断 never 需要阻止分发

ts 复制代码
type IsNever<T> = [T] extends [never] ? true : false;

如果直接写 T extends never,传入 never 会得到 never,不是 true

44. 判断 any 需要特殊技巧

ts 复制代码
type IsAny<T> = 0 extends (1 & T) ? true : false;

any 与交叉类型结合会产生特殊兼容性,可用于识别。

45. 判断 unknown

ts 复制代码
type IsUnknown<T> = unknown extends T
  ? IsAny<T> extends true
    ? false
    : true
  : false;

判断 unknown 时通常要先排除 any

五、联合类型技巧:46-55

46. 联合类型会被自动扁平化

ts 复制代码
type A = string | (number | boolean);

最终等价于 string | number | boolean

47. never 会从联合类型中消失

ts 复制代码
type A = string | never; // string

这使得 never 成为过滤分支的理想返回值。

48. 联合类型转交叉类型

ts 复制代码
type UnionToIntersection<U> = (
  U extends unknown ? (arg: U) => void : never
) extends (arg: infer I) => void
  ? I
  : never;

利用函数参数逆变位置完成转换。

49. 获取联合类型最后一项

ts 复制代码
type LastOfUnion<U> = UnionToIntersection<
  U extends unknown ? () => U : never
> extends () => infer R
  ? R
  : never;

联合类型本身无顺序,所谓"最后一项"是编译器内部表现,不应依赖业务语义。

50. 联合转元组属于高级技巧

ts 复制代码
type UnionToTuple<U, Last = LastOfUnion<U>> = [U] extends [never]
  ? []
  : [...UnionToTuple<Exclude<U, Last>>, Last];

它常用于类型挑战,但生产代码中要谨慎使用。

51. 可辨识联合类型适合状态建模

ts 复制代码
type State =
  | { type: 'loading' }
  | { type: 'success'; data: string }
  | { type: 'error'; message: string };

通过共同字段 type 进行安全收窄。

52. 联合类型收窄依赖控制流分析

ts 复制代码
function handle(state: State) {
  if (state.type === 'success') {
    state.data;
  }
}

类型系统会根据判断条件缩小类型。

53. 穷尽检查使用 never

ts 复制代码
function assertNever(value: never): never {
  throw new Error(String(value));
}

当新增联合分支却没有处理时,never 检查会报错。

54. 联合类型分发可实现批量转换

ts 复制代码
type EventName<T> = T extends string ? `on${Capitalize<T>}` : never;
type R = EventName<'click' | 'focus'>; // 'onClick' | 'onFocus'

模板字符串类型与联合分发结合非常强大。

55. 联合过大可能影响编译性能

大量模板字符串交叉组合会产生指数级联合类型。

ts 复制代码
type Size = 'sm' | 'md' | 'lg';
type Color = 'red' | 'blue' | 'green';
type ClassName = `${Size}-${Color}`;

组合规模要控制,避免类型爆炸。

六、映射类型与对象变换:56-70

flowchart TD A[对象类型 T] --> B[keyof T 获取键] B --> C[遍历每个键 K] C --> D[读取 T[K]] D --> E[生成新对象类型]

56. 映射类型遍历键名

ts 复制代码
type Copy<T> = {
  [K in keyof T]: T[K];
};

这是对象类型转换的基础形式。

57. readonly 修饰符可添加

ts 复制代码
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

对应内置 Readonly

58. readonly 修饰符可移除

ts 复制代码
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

-readonly 表示去掉只读。

59. 可选修饰符可添加

ts 复制代码
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

对应内置 Partial

60. 可选修饰符可移除

ts 复制代码
type RequiredProps<T> = {
  [K in keyof T]-?: T[K];
};

-? 表示去掉可选。

61. Pick 选择部分键

ts 复制代码
type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

这是最常用的对象裁剪工具。

62. Omit 排除部分键

ts 复制代码
type MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

先排除键,再 Pick 剩余键。

63. Record 创建键值映射

ts 复制代码
type MyRecord<K extends keyof any, V> = {
  [P in K]: V;
};

keyof any 等价于 string | number | symbol

64. 键名重映射使用 as

ts 复制代码
type Prefix<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

可把 { name: string } 变成 { getName: () => string }

65. 键名重映射可过滤属性

ts 复制代码
type OnlyStringProps<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

映射到 never 的键会被删除。

66. 对象值类型可统一转换

ts 复制代码
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

适合表单初始值、接口字段兼容等场景。

67. 深度递归对象转换

ts 复制代码
type DeepReadonly<T> = T extends (...args: any[]) => any
  ? T
  : T extends object
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    : T;

递归时要注意函数、数组、特殊对象的处理。

68. 深度可选类型

ts 复制代码
type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

常用于配置覆盖和测试数据构造。

69. 获取可选键

ts 复制代码
type OptionalKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];

通过 {} 是否能赋值给单属性对象判断可选性。

70. 获取必选键

ts 复制代码
type RequiredKeys<T> = Exclude<keyof T, OptionalKeys<T>>;

复用 OptionalKeys 能保持实现简洁。

七、元组与数组:71-82

71. 数组元素类型提取

ts 复制代码
type ArrayItem<T> = T extends readonly (infer Item)[] ? Item : never;

使用 readonly 可兼容普通数组和只读数组。

72. 元组长度是字面量数字

ts 复制代码
type T = [string, number];
type Len = T['length']; // 2

普通数组的 lengthnumber,元组更精确。

73. 提取元组第一项

ts 复制代码
type First<T extends readonly unknown[]> = T extends readonly [infer F, ...unknown[]]
  ? F
  : never;

变长元组是类型体操核心能力之一。

74. 提取元组最后一项

ts 复制代码
type Last<T extends readonly unknown[]> = T extends readonly [...unknown[], infer L]
  ? L
  : never;

从尾部推导需要使用剩余元素语法。

75. 去掉第一项

ts 复制代码
type Shift<T extends readonly unknown[]> = T extends readonly [unknown, ...infer Rest]
  ? Rest
  : [];

常用于递归处理参数列表。

76. 去掉最后一项

ts 复制代码
type Pop<T extends readonly unknown[]> = T extends readonly [...infer Rest, unknown]
  ? Rest
  : [];

Last 的推导位置类似。

77. 元组追加元素

ts 复制代码
type Push<T extends readonly unknown[], V> = [...T, V];

如果输入是只读元组,输出默认是可变元组,可按需加 readonly

78. 元组前置元素

ts 复制代码
type Unshift<T extends readonly unknown[], V> = [V, ...T];

常用于递归构造结果。

79. 元组递归反转

ts 复制代码
type Reverse<T extends readonly unknown[], R extends unknown[] = []> = T extends readonly [
  infer F,
  ...infer Rest
]
  ? Reverse<Rest, [F, ...R]>
  : R;

第二个泛型 R 是累加器。

80. 元组构造数字计数

ts 复制代码
type BuildTuple<N extends number, R extends unknown[] = []> = R['length'] extends N
  ? R
  : BuildTuple<N, [...R, unknown]>;

类型层面没有直接算术,常用元组长度模拟数字。

81. 类型层面加法

ts 复制代码
type Add<A extends number, B extends number> = [
  ...BuildTuple<A>,
  ...BuildTuple<B>
]['length'];

适合小数字,过大数字会触发递归深度限制。

82. 判断数组和元组

ts 复制代码
type IsTuple<T extends readonly unknown[]> = number extends T['length'] ? false : true;

普通数组长度是 number,元组长度是数字字面量。

八、模板字符串类型:83-90

flowchart TD A[字符串字面量] --> B[模板字符串匹配] B --> C[infer 提取片段] C --> D[递归处理剩余字符串] D --> E[生成目标类型]

83. 模板字符串类型可拼接字符串

ts 复制代码
type Event<K extends string> = `on${Capitalize<K>}`;
type Click = Event<'click'>; // 'onClick'

它让字符串协议也能被类型系统约束。

84. 内置大小写工具

ts 复制代码
type A = Uppercase<'abc'>; // 'ABC'
type B = Lowercase<'ABC'>; // 'abc'
type C = Capitalize<'name'>; // 'Name'
type D = Uncapitalize<'Name'>; // 'name'

这些工具只能处理字符串字面量类型。

85. 解析路径参数

ts 复制代码
type Params<Path extends string> = Path extends `${string}:${infer Param}/${infer Rest}`
  ? Param | Params<Rest>
  : Path extends `${string}:${infer Param}`
    ? Param
    : never;

type P = Params<'/user/:id/post/:postId'>; // 'id' | 'postId'

路由库中经常使用此类技巧。

86. 字符串递归替换

ts 复制代码
type ReplaceAll<S extends string, From extends string, To extends string> = From extends ''
  ? S
  : S extends `${infer Head}${From}${infer Tail}`
    ? `${Head}${To}${ReplaceAll<Tail, From, To>}`
    : S;

要特别处理 From 为空字符串,否则会无限递归。

87. 字符串转联合字符

ts 复制代码
type StringToUnion<S extends string> = S extends `${infer C}${infer Rest}`
  ? C | StringToUnion<Rest>
  : never;

适合理解递归,不建议在长字符串上滥用。

88. 去除左侧空白

ts 复制代码
type Space = ' ' | '\n' | '\t';
type TrimLeft<S extends string> = S extends `${Space}${infer Rest}` ? TrimLeft<Rest> : S;

字符串递归通常由"匹配一段 + 处理剩余"组成。

89. 去除右侧空白

ts 复制代码
type TrimRight<S extends string> = S extends `${infer Rest}${Space}` ? TrimRight<Rest> : S;

左右组合即可得到完整 Trim

90. 字符串协议映射对象键

ts 复制代码
type Watcher<T> = {
  [K in keyof T as `${string & K}Changed`]: (value: T[K]) => void;
};

可以把数据字段自动映射成事件名。

九、函数、this 与重载:91-96

91. 获取函数参数类型

ts 复制代码
type MyParameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any
  ? P
  : never;

对应内置 Parameters

92. 获取函数返回值类型

ts 复制代码
type MyReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R
  ? R
  : never;

对应内置 ReturnType

93. 获取构造函数参数

ts 复制代码
type MyConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (
  ...args: infer P
) => any
  ? P
  : never;

abstract new 可兼容抽象类构造签名。

94. 获取实例类型

ts 复制代码
type MyInstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (
  ...args: any
) => infer R
  ? R
  : never;

对应内置 InstanceType

95. 提取 this 参数

ts 复制代码
type MyThisParameterType<T> = T extends (this: infer This, ...args: any[]) => any
  ? This
  : unknown;

TypeScript 允许函数类型显式声明 this 参数。

96. 移除 this 参数

ts 复制代码
type MyOmitThisParameter<T> = unknown extends MyThisParameterType<T>
  ? T
  : T extends (this: any, ...args: infer P) => infer R
    ? (...args: P) => R
    : T;

适合把依赖 this 的函数转换为普通函数签名。

十、实战思维与边界:97-100

97. 类型递归有深度限制

复杂递归类型会触发"Type instantiation is excessively deep and possibly infinite"。

ts 复制代码
type Repeat<T extends unknown[]> = Repeat<[...T, unknown]>;

设计递归类型时必须有明确终止条件。

98. 类型体操要服务可读性

ts 复制代码
type ReadableUser = Pick<User, 'id' | 'name'>;

比起写一个过度通用的深层工具,清晰表达业务意图更重要。

99. 优先复用内置工具类型

常用内置工具包括:PartialRequiredReadonlyPickOmitRecordExcludeExtractNonNullableParametersReturnTypeAwaited

ts 复制代码
type Data = Awaited<Promise<string>>; // string

内置工具经过社区验证,通常比手写更可靠。

100. 类型测试能提高可靠性

ts 复制代码
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
  ? true
  : false;

type Expect<T extends true> = T;
type Case = Expect<Equal<ReturnType<() => string>, string>>;

复杂工具类型建议配套类型测试,避免后续修改破坏推导结果。

十一、常见类型体操流程图

条件类型分发流程

flowchart TD A[联合类型 string 或 number] --> B[进入裸类型参数条件判断] B --> C[string 分支单独判断] B --> D[number 分支单独判断] C --> E[得到 string 对应结果] D --> F[得到 number 对应结果] E --> G[合并为新的联合类型] F --> G[合并为新的联合类型]

对象属性过滤流程

flowchart TD A[对象类型] --> B[遍历 keyof T] B --> C[判断 T K 是否满足条件] C --> D[满足则保留键 K] C --> E[不满足则映射为 never] D --> F[生成新对象] E --> F[生成新对象]

元组递归处理流程

flowchart TD A[输入元组] --> B[匹配第一项和剩余项] B --> C[处理第一项] C --> D[把结果放入累加器] D --> E[继续处理剩余项] E --> B[递归下一轮] B --> F[元组为空时返回累加器]

字符串递归解析流程

flowchart TD A[输入字符串] --> B[模板字符串匹配] B --> C[提取当前片段] C --> D[处理剩余字符串] D --> B[递归匹配] B --> E[无法匹配时结束] E --> F[输出解析结果]

十二、学习建议

  1. 先熟悉 keyof、索引访问、泛型、条件类型、映射类型。
  2. 再练习 infer、联合分发、模板字符串类型和元组递归。
  3. 每写一个复杂工具类型,都加 2 到 5 个类型测试用例。
  4. 遇到难题先画"输入类型 -> 判断条件 -> 推导位置 -> 输出类型"的流程。
  5. 生产代码以可维护为第一原则,能用内置工具就不要重复造轮子。
flowchart TD A[基础类型能力] --> B[泛型与约束] B --> C[条件类型和 infer] C --> D[映射类型和键名重映射] D --> E[元组递归和字符串解析] E --> F[业务类型建模]
相关推荐
喜欢踢足球的老罗1 小时前
一张跨域图的“四次换乘“:blob URL 与 Chrome 扩展架构里的工程艺术
前端·chrome·架构
程序员黑豆1 小时前
AI全栈开发 - Java:基本数据类型 vs 引用数据类型的内存存储
java·前端·ai编程
FserSuN1 小时前
Chrome CORS / PNA / LNA 问题排查与解决方案
前端·chrome
小小小小宇2 小时前
Claude Code 自动运行方法大全
前端
道友可好2 小时前
AI 测试全绿,代码却是错的
前端·人工智能·后端
国科安芯2 小时前
商业航天通信载荷数字处理单元供电架构研究——基于ASP7A84AS的高精度低压差线性稳压器技术分析
前端·单片机·嵌入式硬件·fpga开发·架构·安全性测试
TangentDomain2 小时前
AI 写代码时代,游戏 UI 架构为什么停在 MVP?
前端·游戏·架构
英勇无比的消炎药2 小时前
前端提效神器全新AI组件库TinyRobot改写日常开发模式
前端·vue.js