泛型 & 映射类型 & 条件类型区别

40 阅读5分钟

一、泛型(Generics)

1. 概念

泛型是把类型参数化,让函数/类/接口在保持类型安全的同时变得可复用。常见场景:容器类型、工具函数、库接口、组件 Props。

2. 基本语法

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

const s = identity<string>('hello'); // s: string
const n = identity(123);             // 泛型可被推断,n: number

3. 常见用法

  • 函数泛型
  • 接口/类型别名泛型
  • 类泛型
  • 泛型约束extends
  • 默认类型参数

示例:泛型约束与 keyof

function pluck<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
  return keys.map(k => obj[k]);
}

const user = { id: 1, name: 'Alice' };
const names = pluck(user, ['name']); // 类型安全:T[K] 推断为 string

4. 泛型高级点

  • 约束(constraints) :保证泛型参数满足某些结构(比如有某个属性)。
  • 默认泛型参数type Box<T = string> = { value: T }
  • 联合/交叉与泛型:理解泛型和联合/交叉结合时的推断行为。
  • 推断优先级:当有多个泛型参数时,类型推断的传播规则(有时需显式指定)。
  • 泛型与函数重载/JSX:组件泛型在 React/TSX 下的 tricky 点(常在面试被问)。

5. 常见陷阱

  • 不要滥用 any——用 unknown 更安全。
  • 泛型过宽会丢失具体信息;过窄又失去泛型收益。
  • 不同泛型参数间的关系要明确(例如 TK 的约束)。

二、映射类型(Mapped Types)

1. 概念

映射类型是基于现有类型的键集合,生成一个新类型。TypeScript 的内置工具类型(如 Partial<T>Readonly<T>Record<K,T>)都是映射类型或基于映射类型实现的。

2. 基本语法

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

type Partial<T> = {
  [P in keyof T]?: T[P];
};

[P in keyof T] 遍历 T 的所有键,构造新的属性映射。

3. 常见内置工具类型

  • Partial<T>:把所有属性变成可选
  • Required<T>:把所有属性变成必需
  • Readonly<T>:把所有属性变为 readonly
  • Pick<T, K>:选择子集属性
  • Omit<T, K>:排除某些属性(通常通过 Pick 或条件类型实现)
  • Record<K extends keyof any, T>:从键到值的映射

示例:实现 Omit

type MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// 或用映射类型的 key remapping(TS 4.1+):
type MyOmit2<T, K extends keyof any> = {
  [P in keyof T as P extends K ? never : P]: T[P]
};

4. Key Remapping(TS 4.1+)

可以在映射时重写键名或过滤键:

type Prefixed<T, Prefix extends string> = {
  [K in keyof T as `${Prefix}${string & K}`]: T[K]
};

type A = { id: number; name: string };
type B = Prefixed<A, 'x_'>; // { x_id: number; x_name: string }

5. 严格/可选/readonly 修饰

映射可以结合 + / - 操作符改变修饰符:

type Mutable<T> = { -readonly [P in keyof T]: T[P] };
type Optional<T> = { [P in keyof T]?: T[P] };

三、条件类型(Conditional Types)

1. 概念

条件类型类似类型级别的 if,语法:

T extends U ? X : Y

如果 T 可赋值给 U,条件类型取 X,否则取 Y

2. 基本示例

type IsString<T> = T extends string ? true : false;
type A = IsString<'a'>; // true
type B = IsString<number>; // false

3. 分布式条件类型(distributive conditional types)

当条件类型的左侧是一个裸的类型参数 T(不是被包裹在数组或元组等),并且 T 是联合类型时,条件类型会对联合的每个成员分别分发,再合并结果(即“分布式”)。

type Foo<T> = T extends string ? 'S' : 'N';
type R = Foo<'a' | 1>;
// 等价于 Foo<'a'> | Foo<1> -> 'S' | 'N'

避免分发:将 T 包在元组中即可阻止分发:

type NoDistrib<T> = [T] extends [string] ? 'S' : 'N';

4. 用 infer 做类型推断

在条件类型里用 infer 从某种复合类型中提取子类型:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type F = (x: number) => string;
type R = ReturnType<F>; // string

更多示例:数组元素类型、Promise 解包(Flatten)

type ElementType<T> = T extends (infer U)[] ? U : T;
type E = ElementType<number[]>; // number

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

5. 常见组合用法(映射 + 条件 + infer)

实现深度可选(DeepPartial):

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
};

注意:这里 T[P] extends object 会匹配数组和函数,通常要更精确:

type IsPlainObject<T> = T extends object
  ? T extends Function ? false : true
  : false;

type DeepPartial2<T> = {
  [P in keyof T]?: IsPlainObject<T[P]> extends true ? DeepPartial2<T[P]> : T[P];
};

6. 常用工具类型底层实现

  • Exclude<T, U>:剔除联合中的成员

    type MyExclude<T, U> = T extends U ? never : T;
    
  • Extract<T, U>:提取交集成员

    type MyExtract<T, U> = T extends U ? T : never;
    
  • NonNullable<T>:去除 null|undefined

    type MyNonNullable<T> = T extends null | undefined ? never : T;
    

四、三者结合

“泛型是类型参数化,用于增强复用性和类型安全。

映射类型通过 [P in keyof T] 遍历属性可以生成 Partial/Readonly 这类类型;

条件类型 T extends U ? X : Y 可以做类型分支,注意当左侧是联合时条件会分发(distributive),并且可以用 infer 从复杂类型中提取子类型。

工程中常把三者结合用于 DTO -> Form、API -> Client 类型转换等。

场景:从 API 响应类型生成表单值类型(把所有属性变为可选,数组不展开,函数去掉)

type ApiType = {
  id: number;
  name: string;
  tags: string[];
  meta?: { createdAt: string };
  onClick?: () => void;
};

type Formify<T> = {
  [K in keyof T]?: T[K] extends (...args: any[]) => any // 条件类型过滤函数
    ? never
    : T[K] extends Array<infer U> // 数组处理
      ? Array<Formify<U>>
      : T[K] extends object
        ? Formify<T[K]>
        : T[K];
};

// 结果:{ id?: number; name?: string; tags?: string[]; meta?: { createdAt?: string } }
type FormType = Formify<ApiType>;

这个例子展示了:

  • 映射类型遍历属性
  • 条件类型判断属性类别并用 infer 提取数组元素类型
  • 递归地把嵌套对象也转换
  1. 实现 Partial<T> / Readonly<T> / Pick<T, K> / Omit<T, K>
type MyPartial<T> = {
  [P in keyof T]?: T[P];
};

type MyReadonly<T> = {
  readonly [P in keyof T]: T[P];
};

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

type MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
  1. ReturnType<F>(使用 infer
type MyReturnType<F> = 
  F extends (...args: any[]) => infer R 
    ? R 
    : never;
  1. 解释 T extends U ? X : YT = A | B 时的结果 示例:
T extends U ? X : Y

T 是一个联合类型

type R = ('a' | 1) extends string ? 'YES' : 'NO';

这个表达式 会被分发

相当于:

('a' extends string ? 'YES' : 'NO') 
|
(1 extends string ? 'YES' : 'NO')

结果:

type R = 'YES' | 'NO';
  1. 如何阻止分发

把 T 放在元组中:

type NoDistrib<T> = [T] extends [string] ? 'YES' : 'NO';

type A = NoDistrib<'a' | 1>; // NO(整体比较,不会分发)