TypeScript类型系统地狱

高级类型编程
类型体操训练场

泛型参数的魔法、条件类型的推理、映射类型的变换……掌握这些核心武器,你就能构建编译时就拒绝 Bug的坚不可摧的类型系统。

6 大核心概念
12+ 代码示例
实战驱动

为什么需要「类型体操」?

让编译器成为你最严格的代码审查员

零运行时开销

所有类型检查在编译时完成,打包产物中不包含任何类型信息,对性能零影响。

API 契约

精确的类型定义就是最好的文档,调用者无需翻阅源码就知道如何正确使用。

重构安全网

修改接口或函数签名后,编译器会标出所有受影响的调用点,避免遗漏。

Part 01

泛型 — 类型系统的「函数」

核心思想

泛型允许你编写不预先绑定具体类型的代码,调用时再「注入」类型参数。 可以把它理解为类型的函数——接收类型作为输入,输出新的类型。

1

函数泛型

在函数名后声明 <T>,参数和返回值都可以使用 T

2

接口泛型

让接口的属性类型变成可配置的,如 Array<T>

3

类泛型

类的构造函数和方法共享同一类型参数

4

泛型约束

使用 extends 关键字限制泛型的范围

basic-generics.ts
// 函数泛型:类型安全的 identity 函数
function identity<T>(value: T): T {
  return value;
}

// 调用时显式指定,或让 TS 自动推断
const str = identity<string>("hello");  // string
const num = identity(42);               // number 推断

// 泛型约束:T 必须拥有 length 属性
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(item: T): T {
  console.log(item.length);
  return item;
}

logLength("abc");       // ✅ string 有 length
logLength([1, 2, 3]);   // ✅ array 有 length
// logLength(123);      // ❌ number 没有 length
generic-class.ts
// 泛型类:类型安全的栈
class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }
}

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
// numberStack.push("3"); // ❌ 类型错误
Part 02

条件类型 — 类型的 if-else

语法结构

条件类型的语法形如T extends U ? X : Y,它让类型系统拥有了分支判断的能力。 当 T 可赋值给 U 时结果为 X,否则为 Y。

T extends U ?
✅ true
→ X
|
❌ false
→ Y
conditional-basic.ts
// 基础条件类型
type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">;  // true
type B = IsString<42>;       // false

// 分发条件类型(Distributive)
// 当 T 是联合类型时,条件类型会逐一应用
type ToArray<T> = T extends any ? T[] : never;

type C = ToArray<string | number>;
// = string[] | number[]   ← 分发结果
// 而非 (string | number)[]

// 用 never 过滤类型
type NonNullable2<T> = T extends null | undefined ? never : T;

type D = NonNullable2<string | null | undefined>;
// = string
conditional-advanced.ts
// infer 关键字:在条件类型中「捕获」子类型
// 提取函数返回值类型
type ReturnType2<T> = T extends (...args: any[]) => infer R
  ? R
  : never;

type R1 = ReturnType2<() => string>;       // string
type R2 = ReturnType2<(x: number) => void>; // void

// 提取 Promise 内部类型
type UnwrapPromise<T> = T extends Promise<infer U>
  ? UnwrapPromise<U>   // 递归解包
  : T;

type U1 = UnwrapPromise<Promise<Promise<number>>>;
// = number

// 提取数组元素类型
type ElementType<T> = T extends (infer E)[] ? E : never;

type E1 = ElementType<string[]>;  // string
type E2 = ElementType<[1, "a"]>;  // 1 | "a"
Part 03

映射类型 — 批量变换类型属性

核心机制

映射类型基于索引签名语法,遍历一个已有类型的所有键, 并对每个键的值类型进行变换,从而批量生成新类型

内置工具类型速查

Partial<T>所有属性变为可选 ?
Required<T>所有属性变为必填
Readonly<T>所有属性变为只读 readonly
Pick<T, K>从 T 中挑选部分键 K
Omit<T, K>从 T 中移除部分键 K
Record<K, V>构造键为 K、值为 V 的类型
mapped-basic.ts
// 手动实现 Partial<T>
type Partial2<T> = {
  [K in keyof T]?: T[K];
};

// 手动实现 Readonly<T>
type Readonly2<T> = {
  readonly [K in keyof T]: T[K];
};

// 用 +/- 修饰符控制可选性和只读性
type Required2<T> = {
  [K in keyof T]-?: T[K];  // 移除 ?
};

type Mutable<T> = {
  -readonly [K in keyof T]: T[K]; // 移除 readonly
};

interface User {
  name: string;
  age?: number;
  readonly id: number;
}

type ReqUser = Required2<User>;
// { name: string; age: number; readonly id: number }

type MutableUser = Mutable<User>;
// { name: string; age?: number; id: number }
mapped-advanced.ts
// 键重映射 (as 子句) — TS 4.1+
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person {
  name: string;
  age: number;
}

type PersonGetters = Getters<Person>;
// {
//   getName: () => string;
//   getAge: () => number;
// }

// 过滤键:as 子句返回 never 即可排除
type RemoveByType<T, U> = {
  [K in keyof T as T[K] extends U ? never : K]: T[K];
};

type OnlyStrings = RemoveByType<Person, number>;
// { name: string }  ← age 被排除
Part 04

模板字面量类型 — 字符串级的类型运算

类型也能拼字符串?

TypeScript 4.1 引入模板字面量类型,允许你像写template literal一样在类型层面拼接字符串。结合映射类型条件类型,威力巨大。

template-literal.ts
// 基础拼接
type Greeting = `Hello, ${string}!`;
const g1: Greeting = "Hello, World!"; // ✅
// const g2: Greeting = "Hi, World!"; // ❌

// 结合联合类型 → 笛卡尔积
type Color = "red" | "green" | "blue";
type Size = "sm" | "md" | "lg";

type ColorSize = `${Color}-${Size}`;
// "red-sm" | "red-md" | "red-lg" |
// "green-sm" | ... | "blue-lg"
// 共 3×3 = 9 种组合

// 内置字符串工具类型
type Upper = Uppercase<"hello">;       // "HELLO"
type Lower = Lowercase<"HELLO">;       // "hello"
type Cap   = Capitalize<"hello">;      // "Hello"
type Uncap = Uncapitalize<"Hello">;    // "hello"

// 实战:自动推导事件处理器名称
type PropEventSource<T> = {
  [K in keyof T as `on${Capitalize<string & K>}Change`]: (
    value: T[K]
  ) => void;
};

interface FormData {
  username: string;
  age: number;
}

type FormEvents = PropEventSource<FormData>;
// {
//   onUsernameChange: (value: string) => void;
//   onAgeChange: (value: number) => void;
// }
Part 05

infer 关键字 — 类型的模式匹配

工作原理

infer 允许你在条件类型的 extends 子句中声明一个待推断的类型变量。编译器会尝试将实际类型与模式匹配,并把匹配到的部分 「绑定」到这个变量上。

函数参数推断

T extends (...args: infer P) => any

P = 参数元组类型

函数返回值推断

T extends (...args: any[]) => infer R

R = 返回值类型

Promise 内部推断

T extends Promise<infer U>

U = Promise 包裹的类型

数组元素推断

T extends (infer E)[]

E = 数组元素类型

构造函数推断

T extends new (...args: any[]) => infer I

I = 实例类型

infer-examples.ts
// 提取元组第一个元素
type Head<T extends any[]> = T extends [infer H, ...any[]]
  ? H
  : never;

type H1 = Head<[string, number, boolean]>; // string

// 提取元组最后一个元素
type Last<T extends any[]> = T extends [...any[], infer L]
  ? L
  : never;

type L1 = Last<[string, number, boolean]>; // boolean

// 递归:反转元组
type Reverse<T extends any[]> = T extends [infer H, ...infer Rest]
  ? [...Reverse<Rest>, H]
  : [];

type R = Reverse<[1, 2, 3]>; // [3, 2, 1]

// 提取嵌入错误结果的类型
type ApiResult<T> =
  | { success: true; data: T }
  | { success: false; error: string };

type ExtractData<T> = T extends { success: true; data: infer D }
  ? D
  : never;

type Data = ExtractData<ApiResult<User>>;
// = User
Part 06

递归类型 — 无限嵌套的类型力量

用递归解决复杂类型变换

当你需要对深层嵌套的结构(如 JSON、嵌套数组、路径字符串)做类型变换时, 递归类型是唯一的解法。核心思路:在条件类型中引用自身,设置好终止条件即可。

deep-readonly.ts
// 深度 Readonly
type DeepReadonly<T> = T extends object
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

interface Config {
  db: {
    host: string;
    port: number;
    options: {
      ssl: boolean;
      pool: number;
    };
  };
}

type ReadonlyConfig = DeepReadonly<Config>;
// 所有层级的属性都变为 readonly

// 深度 Partial
type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;
path-type.ts
// 用递归生成对象路径联合类型
type Paths<T, Prefix extends string = ""> = T extends object
  ? {
      [K in keyof T & string]:
        | `${Prefix}${K}`
        | Paths<T[K], `${Prefix}${K}.`>;
    }[keyof T & string]
  : never;

interface State {
  user: {
    name: string;
    address: {
      city: string;
      zip: string;
    };
  };
  theme: "light" | "dark";
}

type StatePaths = Paths<State>;
// "user" | "user.name" | "user.address"
// | "user.address.city" | "user.address.zip"
// | "theme"

// 类型安全的 deep get
function deepGet<T, P extends Paths<T>>(
  obj: T,
  path: P
): /* ... 返回值类型可进一步推导 */ any {
  return path.split(".").reduce(
    (o, k) => (o as any)?.[k], obj
  );
}
Playground

类型体操挑战

1进阶

实现 ReadonlyDeep

递归遍历对象所有层级,让每个属性都变成 readonly。

条件类型 + 递归 + 映射类型
2进阶

实现 Trim<T>

去除字符串字面量类型首尾的空格。

模板字面量 + 条件类型 + infer
3地狱

实现 TupleToNestedObject

给定元组 [string, number, boolean] 和类型 V,生成 { string: { number: { boolean: V } } }。

递归元组 + 映射类型
4地狱

实现 RequiredKeys<T>

提取一个类型中所有必填(非可选)的键名组成的联合类型。

条件类型 + 映射类型 + never 过滤
5地狱

实现 ParseQueryString

将字符串 'a=1&b=2&c=3' 解析为类型 { a: '1'; b: '2'; c: '3' }。

模板字面量 + infer + 递归
6进阶

实现 DeepOmit<T, K>

在所有嵌套层级中递归地移除键 K。

递归 + Omit + 条件类型
Roadmap

学习路径建议

Step 1

掌握泛型基础

函数泛型、接口泛型、泛型约束。理解类型参数的生命周期和推断机制。

Step 2

理解条件类型 & infer

学会用 extends 做类型分支判断,用 infer 做模式匹配和类型提取。

Step 3

深入映射类型

理解索引签名、键重映射(as)、修饰符 +/-。能手写 Partial/Readonly/Pick/Omit。

Step 4

模板字面量 & 递归

掌握字符串级别的类型运算,学习用递归处理嵌套结构。

Step 5

实战综合运用

在真实项目中用高级类型封装 API 客户端、表单验证器、状态管理器等。

推荐学习资源

官方

TypeScript 官方手册

Advanced Types 章节是第一手权威资料

练习

type-challenges

GitHub 上最流行的类型体操题库,由易到难

课程

Total TypeScript

Matt Pocock 的付费课程,实战导向的进阶教程

书籍

深入理解 TypeScript

国内社区整理的免费电子书,覆盖全面

准备好开始你的类型体操之旅了吗?

从 type-challenges 的 Easy 题开始,循序渐进,每天刷几道, 不久你就能驾驭任何复杂的类型场景。