前置知识: TypeScript

条件类型与映射类型

2 minAdvanced2026/6/13

TypeScript条件类型、infer关键字、映射类型、模板字面量类型与类型体操实战。

1. 条件型基础

1.1 基本语法

条件型根据型条件选择不同的型结果,型层面的三元表达式。

// 基本语法: T extends U ? X : Y
type IsString<T> = T extends string ? true : false;

type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<'hello'>; // true(字面量类型也匹配)

// 实用示例:提取Promise的值类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type P1 = UnwrapPromise<Promise<string>>; // string
type P2 = UnwrapPromise<Promise<number[]>>; // number[]
type P3 = UnwrapPromise<string>; // string(非Promise直接返回)

1.2 条件型的分发特性

// 当T是联合类型时,条件类型会自动分发
type ToArray<T> = T extends unknown ? T[] : never;

type Result = ToArray<string | number>; // string[] | number[]
// 分发过程:
// ToArray<string> | ToArray<number>
// string[] | number[]

// 阻止分发:用元组包裹
type ToArrayNoDistribute<T> = [T] extends [unknown] ? T[] : never;

type Result2 = ToArrayNoDistribute<string | number>; // (string | number)[]

1.3 实用条件

// NonNullable: 排除null和undefined
type NN = NonNullable<string | null | undefined>; // string

// Extract: 从联合类型中提取匹配的类型
type T1 = Extract<'a' | 'b' | 'c', 'a' | 'b'>; // 'a' | 'b'
type T2 = Extract<string | number | (() => void), Function>; // () => void

// Exclude: 从联合类型中排除匹配的类型
type T3 = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
type T4 = Exclude<string | number | boolean, number>; // string | boolean

// ReturnType: 获取函数返回类型
type R1 = ReturnType<() => string>; // string
type R2 = ReturnType<(x: number) => void>; // void

// Parameters: 获取函数参数类型(元组)
type P1 = Parameters<(a: string, b: number) => void>; // [string, number]

// InstanceType: 获取构造函数实例类型
class MyClass {
  x = 10;
  y = 20;
}
type Instance = InstanceType<typeof MyClass>; // { x: number; y: number }

2. infer 关键字

2.1 基本用法

infer 在条件型的 extends 子句中声明型变量,用于推断型。

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

// 提取函数第一个参数类型
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type F1 = FirstArg<(name: string, age: number) => void>; // string

// 提取Promise链的最终值
type DeepUnwrapPromise<T> = T extends Promise<infer U> ? DeepUnwrapPromise<U> : T;

type Deep = DeepUnwrapPromise<Promise<Promise<Promise<number>>>>; // number

2.2 多个 infer

// 提取函数的参数和返回值
type FunctionInfo<T> = T extends (...args: infer Args) => infer Return
  ? { args: Args; return: Return }
  : never;

type Info = FunctionInfo<(name: string, age: number) => boolean>;
// { args: [string, number]; return: boolean }

// 提取元组的第一个和剩余元素
type HeadTail<T extends any[]> = T extends [infer Head, ...infer Tail]
  ? { head: Head; tail: Tail }
  : never;

type HT = HeadTail<[1, 2, 3, 4]>;
// { head: 1; tail: [2, 3, 4] }

2.3 infer 约束(TypeScript 4.7+)

// 限制infer推断的类型
type FirstIfString<T> = T extends [infer S extends string, ...unknown[]] ? S : never;

type A = FirstIfString<['hello', 1, 2]>; // 'hello'
type B = FirstIfString<[1, 'hello', 2]>; // never(第一个不是string)

3. 映射

3.1 基本映射

// 遍历联合类型创建对象类型
type Record<K extends string | number | symbol, V> = {
  [P in K]: V;
};

// 基本映射
type StringMap = Record<'name' | 'email', string>;
// { name: string; email: string }

// 遍历键创建新类型
type Getters<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};

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

type UserGetters = Getters<User>;
// {
//   getId: () => number;
//   getName: () => string;
//   getEmail: () => string;
// }

3.2 映射型修饰符

// Partial: 所有属性变为可选
type MyPartial<T> = {
  [P in keyof T]?: T[P];
};

// Required: 所有属性变为必选
type MyRequired<T> = {
  [P in keyof T]-?: T[P]; // -? 移除可选修饰符
};

// Readonly: 所有属性变为只读
type MyReadonly<T> = {
  readonly [P in keyof T]: T[P];
};

// Mutable: 移除只读修饰符
type Mutable<T> = {
  -readonly [P in keyof T]: T[P]; // -readonly 移除只读
};

interface Config {
  readonly host: string;
  readonly port: number;
  debug?: boolean;
}

type MutableConfig = Mutable<Config>;
// { host: string; port: number; debug?: boolean }

type RequiredConfig = Required<Config>;
// { readonly host: string; readonly port: number; debug: boolean }

3.3 键重映射(Key Remapping)

// as 子句重映射键
type RemovePrefix<T, P extends string> = {
  [K in keyof T as K extends `${P}${infer Rest}` ? Rest : K]: T[K];
};

interface PrefixedEnv {
  VITE_API_URL: string;
  VITE_APP_TITLE: string;
  NODE_ENV: string;
}

type EnvVars = RemovePrefix<PrefixedEnv, 'VITE_'>;
// { API_URL: string; APP_TITLE: string; NODE_ENV: string }

// 过滤属性
type OnlyStrings<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

interface Mixed {
  name: string;
  age: number;
  email: string;
  active: boolean;
}

type StringProps = OnlyStrings<Mixed>;
// { name: string; email: string }

4. 模板字面量

4.1 基本用法

// 字符串拼接类型
type EventName = 'click' | 'focus' | 'blur';
type HandlerName = `on${Capitalize<EventName>}`;
// 'onClick' | 'onFocus' | 'onBlur'

// CSS属性类型
type CSSProperty = 'width' | 'height' | 'margin' | 'padding';
type CSSDirection = 'top' | 'right' | 'bottom' | 'left';
type CSSRule = `${CSSProperty}-${CSSDirection}`;
// 'margin-top' | 'margin-right' | ... | 'padding-left'

// 内置字符串操作类型
type UppercaseA = Uppercase<'hello'>; // 'HELLO'
type LowercaseA = Lowercase<'HELLO'>; // 'hello'
type CapitalizeA = Capitalize<'hello'>; // 'Hello'
type UncapitalizeA = Uncapitalize<'Hello'>; // 'hello'

4.2 实用模板字面量

// 事件监听器类型
type EventMap = {
  click: { x: number; y: number };
  keydown: { key: string; code: string };
  resize: { width: number; height: number };
};

type EventHandler<T extends keyof EventMap> = (event: EventMap[T]) => void;

function on<T extends keyof EventMap>(event: T, handler: EventHandler<T>): void {
  // 实现
}

on('click', (e) => {
  console.log(e.x, e.y); // 类型安全
});

// 路由参数类型
type ExtractRouteParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}`
  ? { [K in Param | keyof ExtractRouteParams<Rest>]: string }
  : T extends `${string}:${infer Param}`
    ? { [K in Param]: string }
    : {};

type Route1 = ExtractRouteParams<'/users/:id'>; // { id: string }
type Route2 = ExtractRouteParams<'/posts/:postId/comments/:commentId'>;
// { postId: string; commentId: string }

5. 型体操实战

5.1 DeepPartial

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

interface Config {
  database: {
    host: string;
    port: number;
    credentials: {
      username: string;
      password: string;
    };
  };
  cache: {
    enabled: boolean;
    ttl: number;
  };
}

type PartialConfig = DeepPartial<Config>;
// 所有嵌套属性都是可选的
const config: PartialConfig = {
  database: {
    credentials: {
      username: 'admin',
    },
  },
};

5.2 PickByType

// 按值类型选取属性
type PickByType<T, ValueType> = {
  [K in keyof T as T[K] extends ValueType ? K : never]: T[K];
};

interface User {
  name: string;
  age: number;
  email: string;
  active: boolean;
  score: number;
}

type StringProps = PickByType<User, string>; // { name: string; email: string }
type NumberProps = PickByType<User, number>; // { age: number; score: number }

5.3 OmitByType

type OmitByType<T, ValueType> = {
  [K in keyof T as T[K] extends ValueType ? never : K]: T[K];
};

type NonStringProps = OmitByType<User, string>;
// { age: number; active: boolean; score: number }

6. 常见问题与解决方案

6.1 条件型分发导致意外结果

// 问题:分发导致结果不是预期的
type WrapInArray<T> = T extends unknown ? T[] : never;
type R = WrapInArray<string | number>; // string[] | number[]

// 解决方案:阻止分发
type WrapInArrayNoDist<T> = [T] extends [unknown] ? T[] : never;
type R2 = WrapInArrayNoDist<string | number>; // (string | number)[]

6.2 映射型与索引签名

// 映射类型生成的是索引签名,不是具体属性
type StringRecord = Record<string, string>;
// 等价于 { [key: string]: string }

// 具体键的Record
type SpecificRecord = Record<'a' | 'b', string>;
// { a: string; b: string }

7. 总结与最佳实践

7.1 型工具选择

需求工具
条件选择条件型 + extends
提取infer 关键字
转换对象映射
转换键名键重映射 as
字符串型操作模板字面量

7.2 最佳实践

  1. 优先使用内置工具类型:Partial、Required、Pick、Omit等
  2. 复杂类型添加注释:说明型变换的意
  3. 注意分发特性联合类型传入条件型会分发
  4. infer用于提取:从复杂型中提取子
  5. 渐进式复杂度:简单型够用时不要过度抽象