Skip to content

第 10 章 · TypeScript 高级类型

一句话开篇:基础类型让你"写出能跑的代码",高级类型让你"写出能复用的库"——本章带你从写类型走到计算类型


0. 生活类比:从"快餐盒"到"智能流水线"

回想上一章我们已经会用 interfacetypeunion,相当于会用碗、盘子、调味瓶。但要开一家连锁餐厅,你需要的是模具自动化流水线

高级类型生活类比
泛型 Generic"不限菜品的快餐盒"——同一个盒子可以装牛排、可以装沙拉、可以装意面,不变的是"盒子",变的是"内容"
条件类型 T extends U ? X : Y"智能开关"——根据输入自动选择输出
映射类型 [K in keyof T]"复印机+滤镜"——把一个对象的所有字段复印一份并加工
infer 关键字"侦探"——从复杂类型里推断出某一段
工具类型 Utility"官方预制配件",比如 PartialPick,开箱即用
类型体操"用类型写代码"——纯类型层面做"编译器编程"

一句话:高级类型 = 在类型层面写函数。输入是类型,输出还是类型。


1. 泛型(Generic)

1.1 概念是什么

泛型 = 可参数化的类型——把"类型"当成函数的参数传进来。

ts
function identity<T>(x: T): T {
  return x;
}

identity<string>("hi");  // T = string,返回 string
identity(42);            // 自动推断 T = number

类比:泛型函数 = 不限菜品的外卖盒——不管你点什么菜,盒子都能装。

1.2 为什么需要泛型

不用泛型时,你只能:

  • 写一个 numberArray,再写一个 stringArray……重复劳动
  • any,关掉类型检查……失去 IDE 提示
ts
// ❌ 失去类型
function first(arr: any[]): any { return arr[0]; }
const x = first([1, 2, 3]);   // x: any

// ✅ 用泛型保留类型
function first<T>(arr: T[]): T { return arr[0]; }
const y = first([1, 2, 3]);   // y: number ✅

1.3 泛型约束(extends)

让泛型"必须有某个属性":

ts
function getLength<T extends { length: number }>(x: T): number {
  return x.length;
}

getLength("hello");        // ✅
getLength([1, 2, 3]);      // ✅
getLength({ length: 7 });  // ✅
getLength(123);            // ❌ number 没有 length

1.4 多泛型 + 默认类型

ts
function pair<A, B = A>(a: A, b: B): [A, B] {
  return [a, b];
}

pair(1, "x");        // [number, string]
pair(1, 2);          // [number, number]
pair<string>("a");   // ❌ 缺第二个参数

1.5 泛型接口 / 泛型类

ts
interface ApiResp<T> {
  code: number;
  data: T;
}
const r: ApiResp<{ id: number }> = { code: 0, data: { id: 1 } };

class Stack<T> {
  private items: T[] = [];
  push(x: T) { this.items.push(x); }
  pop(): T | undefined { return this.items.pop(); }
}
const s = new Stack<number>();
s.push(1); s.pop(); // returns number | undefined

2. 条件类型 + infer

2.1 条件类型:类型版的三元

ts
type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<"hello">;  // "yes"
type B = IsString<123>;      // "no"

类比:T extends U ? X : Y = 智能开关——T 像 U 就走 X,不像就走 Y。

2.2 分发条件类型

T 是裸类型参数(没有被包成元组等),联合会"自动分发":

ts
type ToArray<T> = T extends any ? T[] : never;

type R = ToArray<string | number>;
// 等价于 ToArray<string> | ToArray<number>
// = string[] | number[]

关闭分发,把 T 包成元组即可:

ts
type ToArray2<T> = [T] extends [any] ? T[] : never;
type R2 = ToArray2<string | number>; // (string | number)[]

2.3 infer:类型层面的"模式匹配"

infer X = "让 TS 帮我把这一段类型起个名叫 X"。

ts
// 取数组元素类型
type ElementOf<T> = T extends (infer U)[] ? U : never;
type N = ElementOf<number[]>;  // number

// 取函数返回值
type ReturnTypeX<T> = T extends (...args: any) => infer R ? R : never;
type FR = ReturnTypeX<() => string>; // string

// 取 Promise 内部类型
type Awaited2<T> = T extends Promise<infer U> ? U : T;
type AW = Awaited2<Promise<number>>; // number

ASCII 图——条件类型流程:

        T 是数组吗?

   ┌───────┴────────┐
   │ T extends (infer U)[] │
   ▼ Yes            │ No
   返回 U           │

                  返回 never

3. 映射类型(Mapped Types)

一句话:遍历一个类型的所有 key,生成一个新类型

3.1 基本写法 [K in keyof T]

ts
type Mutable<T> = { [K in keyof T]: T[K] };
// 等价于复制原来的所有字段

3.2 加修饰符:? readonly-? -readonly

ts
type MyPartial<T>   = { [K in keyof T]?: T[K] };       // 全部可选
type MyRequired<T>  = { [K in keyof T]-?: T[K] };      // 去掉可选
type MyReadonly<T>  = { readonly [K in keyof T]: T[K] };
type Mutable<T>     = { -readonly [K in keyof T]: T[K] };

3.3 重映射 key(as

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

interface Person { name: string; age: number }
type PG = Getters<Person>;
// {
//   getName: () => string;
//   getAge: () => number;
// }

类比:映射类型 = 复印机+滤镜——一边复印,一边给每个 key/value 上一层加工。


4. 工具类型大全

TS 内置的"官方预制配件",几乎天天用:

工具作用输入输出
Partial<T>全部字段可选{ a: 1; b: 2 }{ a?; b? }
Required<T>全部字段必填{ a?; b? }{ a; b }
Readonly<T>全部字段只读{ a }{ readonly a }
Pick<T, K>挑出指定字段{ a; b; c }, "a" | "b"{ a; b }
Omit<T, K>去掉指定字段{ a; b; c }, "c"{ a; b }
Record<K, V>构造索引对象"x" | "y", number{ x: number; y: number }
Exclude<U, V>联合中剔除"a" | "b" | "c", "a""b" | "c"
Extract<U, V>联合中保留"a" | "b" | "c", "a" | "c""a" | "c"
NonNullable<T>去掉 null/undefinedstring | nullstring
ReturnType<F>取函数返回类型() => numbernumber
Parameters<F>取函数参数元组(a: 1, b: 2) => any[1, 2]
Awaited<T>拆 Promise(递归)Promise<Promise<number>>number
Uppercase<S>字符串大写"abc""ABC"
Capitalize<S>首字母大写"abc""Abc"

4.1 实战:业务表单的常用组合

ts
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

// 创建用户的入参:不要 id(后端生成),password 必填
type CreateUserDTO = Omit<User, "id">;

// 更新用户:所有字段可选,但 id 必须有
type UpdateUserDTO = Partial<User> & { id: number };

// 对外返回:去掉密码
type PublicUser = Omit<User, "password">;

// 字典:以用户 id 索引完整对象
type UserMap = Record<number, PublicUser>;

5. 类型体操入门

5.1 手写 MyPartial

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

type T1 = MyPartial<{ a: 1; b: 2 }>; // { a?: 1; b?: 2 }

5.2 手写 MyPick

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

type T2 = MyPick<{ a: 1; b: 2; c: 3 }, "a" | "c">; // { a:1; c:3 }

5.3 手写 MyExclude

ts
type MyExclude<T, U> = T extends U ? never : T;

type T3 = MyExclude<"a" | "b" | "c", "a">; // "b" | "c"

关键:联合分发 + 条件类型,命中 U 的成员变成 never,自然消失。

5.4 手写 MyReturnType

ts
type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never;

type T4 = MyReturnType<() => string>; // string

5.5 手写 DeepReadonly(深只读)

ts
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function ? T[K] : DeepReadonly<T[K]>
    : T[K];
};

type Cfg = DeepReadonly<{ a: { b: { c: number } } }>;
// 三层 readonly,全部字段都不能修改

类比:类型体操 = 用类型积木拼电路——电路本身不"运行",但它会指挥真正的代码。


6. 模块声明 / d.ts 文件

6.1 d.ts 是什么

.d.ts 文件 = 只有类型、没有实现的声明文件。让 TS 知道某段 JS 代码"长什么样"。

6.2 常见三种用途

① 给纯 JS 库写类型

ts
// types/lodash-shim.d.ts
declare module "lodash-shim" {
  export function chunk<T>(arr: T[], size: number): T[][];
}

② 全局类型增强

ts
// types/global.d.ts
declare global {
  interface Window {
    __APP_VERSION__: string;
  }
}
export {};

③ 资源文件类型(Webpack/Vite)

ts
// types/assets.d.ts
declare module "*.svg" {
  const url: string;
  export default url;
}
declare module "*.css";

6.3 让 TS 找到 d.ts

tsconfig.json

json
{
  "compilerOptions": {
    "typeRoots": ["./node_modules/@types", "./types"]
  }
}

7. ⚠️ 易踩的坑

  1. 滥用泛型:当函数只用一种类型时,不要为了"显得专业"加泛型——会让签名更复杂。
  2. 条件类型分发陷阱:联合类型在裸类型参数下会自动分发,常常不是你想要的。要禁用分发就包成元组:[T] extends [U]
  3. keyof any = string | number | symbol,不是你想象的"所有可能的 key"。
  4. Partial<T> 是浅可选:嵌套对象内层不变,要"深可选"得自己写 DeepPartial
  5. Readonly<T> 也是浅只读:内层数组依然可 push。
  6. infer 必须配合 extends:单独写 infer R 没意义。
  7. 类型体操别炫技:业务代码里能用工具类型解决就别手撸;除非真有复杂推断需求。
  8. d.ts 里写 declare module "xxx" 后整个项目共享:注意命名冲突,必要时放 namespace

8. 一句话总结

TS 的高级类型让你在类型系统里"再写一遍代码"——一次定义,处处复用,用编译器把重复的工作全干完。


9. 延伸阅读