Chapter 06

高级类型:Mapped/Conditional/Template Literal

深入高级类型系统,掌握映射类型、条件类型、infer 关键字与模板字面量类型

映射类型(Mapped Types)

映射类型的本质

映射类型是 TypeScript 类型系统中最强大的元编程工具之一。它让你可以基于已有类型机械式地推导出新类型——不需要手写每个属性,而是用一个"模板公式"遍历所有属性并变换。

语法:[K in keyof T]: T[K]
K 是属性名的"迭代变量",类似 for...in 循环;keyof T 得到 T 所有属性名的联合类型;T[K] 是索引访问,获取属性对应的值类型。整体构成一个新类型,每个属性从 T 中复制并可加修饰符(readonly / ?)。
修饰符前缀 +/-
在 ? 或 readonly 前加 - 可以移除该修饰符:-? 移除可选(使属性必填),-readonly 移除只读。+? 和 +readonly 等同于直接写 ? 和 readonly。
as 子句(键名重映射,TS 4.1+)
在 [K in keyof T as NewKey]: ... 中,as 子句可以对键名做变换(如用模板字面量改名),或通过返回 never 过滤掉特定属性。

基本语法

映射类型允许你基于已有类型创建新类型,通过遍历已有类型的每个属性,对属性类型进行变换。语法是 { [K in keyof T]: ... }

type User = { id: number; name: string; email: string };

// 手动实现 Partial(所有属性变可选)
type MyPartial<T> = {
  [K in keyof T]?: T[K];  // ? 使属性可选
};

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

// 移除可选(Required 的实现)
type MyRequired<T> = {
  [K in keyof T]-?: T[K];  // -? 移除可选
};

// 实用:将所有值类型变为 Promise
type Promisify<T> = {
  [K in keyof T]: Promise<T[K]>;
};
type AsyncUser = Promisify<User>;
// { id: Promise<number>; name: Promise<string>; email: Promise<string> }

键名重映射(Key Remapping)

// as 子句:对键名进行变换
type Getters<T> = {
  // 将每个属性名 K 变换为 getK(如 name → getName)
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

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

// 过滤属性:as 子句返回 never 时该属性被排除
type OmitNever<T> = {
  // 只保留值类型不为 never 的属性
  [K in keyof T as T[K] extends never ? never : K]: T[K];
};

// 只保留 string 类型的属性(过滤掉 number/boolean 等)
type StringProperties<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};
type UserStrings = StringProperties<User>;
// { name: string; email: string }(去掉了 id: number)

条件类型(Conditional Types)

条件类型的原理

条件类型的语法是 T extends U ? A : B,含义是:如果类型 T 可以赋值给类型 U,则结果为 A,否则为 B。它本质上是类型系统中的"三元运算符",让你可以根据类型间的关系动态计算出结果类型。

分配律(Distributive):最容易踩坑的特性

当 T 是裸类型参数(naked type parameter,直接写 T 而不是 T[]、[T]、Promise<T> 等包装形式)且传入联合类型时,条件类型会自动分配:TypeScript 将联合的每个成员分别代入求值,再把结果联合起来。

示例:ToArray<string | number> 等同于 ToArray<string> | ToArray<number> = string[] | number[],而不是 (string | number)[]

如果你不想要分配行为,把 T 包裹起来:[T] extends [U] ? A : B,这样联合类型整体参与比较,不会分配。

// 基本语法:T extends U ? A : B
type IsArray<T> = T extends any[] ? true : false;
type R1 = IsArray<string[]>;  // true
type R2 = IsArray<string>;    // false

// NonNullable 的实现:排除 null 和 undefined
type MyNonNullable<T> = T extends null | undefined ? never : T;
// 利用分配律:MyNonNullable<string | null | undefined>
// = (string extends null|undefined ? never : string)
// | (null extends null|undefined ? never : null)
// | (undefined extends null|undefined ? never : undefined)
// = string | never | never = string

// 分配式条件类型(Distributive)
// 当 T 是联合类型时,条件类型会分别对每个成员求值再联合
type ToArray<T> = T extends any ? T[] : never;
type R3 = ToArray<string | number>;  // string[] | number[]

// 禁用分配:用元组包裹,让整个联合参与比较
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type R4 = ToArrayNonDist<string | number>;  // (string | number)[]

// 实用工具:从联合类型中提取/排除成员(利用 never 过滤)
type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;
type Strings = Extract<string | number | boolean, string>;  // string
type NonStrings = Exclude<string | number | boolean, string>;  // number | boolean

infer:在条件类型中推断类型

infer 关键字只能在条件类型的 extends 子句中使用。它告诉 TypeScript:"我不知道这个位置的类型,请你帮我推断,并把推断结果绑定到这个类型变量上供我使用"。这让条件类型从一个"布尔判断"变成了"类型提取器"。

// 提取函数返回类型(ReturnType 的手动实现)
// 读法:如果 T 是一个函数类型(参数任意),推断其返回类型为 R,结果就是 R
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type R5 = MyReturnType<() => string>;           // string
type R6 = MyReturnType<(x: number) => boolean>; // boolean

// 提取函数参数类型(Parameters 的实现)
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
type Params = MyParameters<(a: string, b: number) => void>;
// [a: string, b: number]

// 提取 Promise 的值类型(Awaited 的实现原型)
// 若 T 是 Promise<V>,推断 V;否则 T 原样返回
type Unwrap<T> = T extends Promise<infer V> ? V : T;
type R7 = Unwrap<Promise<string>>;  // string
type R8 = Unwrap<number>;           // number(不是 Promise,原样返回)

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

// 实战:提取构造函数的实例类型(InstanceType 的实现)
type MyInstanceType<T> = T extends new (...args: any[]) => infer I ? I : never;
class Animal { name = ''; }
type AnimalInstance = MyInstanceType<typeof Animal>;  // Animal

模板字面量类型(Template Literal Types)

TypeScript 4.1 引入的模板字面量类型让你可以在类型层面做字符串拼接和变换。它的语法和 JavaScript 的模板字符串完全相同,但操作的是类型而不是值。当模板中包含联合类型时,TypeScript 会自动做笛卡尔积展开,生成所有可能的字符串字面量类型。

// TypeScript 4.1+ 特性
type EventName = `on${Capitalize}`;
// 'onClick' | 'onChange' | ... (无限集合)

// 实际应用:生成 CSS 属性类型
type Side = 'top' | 'bottom' | 'left' | 'right';
type Padding = `padding-${Side}`;
// 'padding-top' | 'padding-bottom' | 'padding-left' | 'padding-right'

// 生成事件处理器类型
type HTMLTag = 'div' | 'span' | 'button';
type EventHandlers = {
  [K in `on${Capitalize}Click`]: () => void;
};
// { onDivClick: () => void; onSpanClick: () => void; onButtonClick: () => void }

// 类型安全的 i18n key
type Lang = 'en' | 'zh' | 'ja';
type Namespace = 'common' | 'auth' | 'dashboard';
// 笛卡尔积:3 × 3 = 9 个字面量类型
type TranslationKey = `${Lang}.${Namespace}`;
// 'en.common' | 'en.auth' | 'en.dashboard' | 'zh.common' | ...

内置字符串操纵类型

TypeScript 内置了 4 个专门用于模板字面量的字符串变换工具类型:

Uppercase<S>
将字符串字面量类型转为全大写。例:Uppercase<'hello'> → 'HELLO'
Lowercase<S>
将字符串字面量类型转为全小写。例:Lowercase<'HELLO'> → 'hello'
Capitalize<S>
将字符串字面量类型的首字母大写。例:Capitalize<'hello'> → 'Hello'
Uncapitalize<S>
将字符串字面量类型的首字母小写。例:Uncapitalize<'Hello'> → 'hello'
// 实战:从对象类型自动生成事件类型
type ModelEvents<T> = {
  // on + 属性名首字母大写 + Change → onChange 事件回调
  [K in keyof T as `on${Capitalize<string & K>}Change`]:
    (newVal: T[K], oldVal: T[K]) => void;
};

type UserModel = { name: string; age: number; active: boolean };
type UserEvents = ModelEvents<UserModel>;
// {
//   onNameChange: (newVal: string, oldVal: string) => void;
//   onAgeChange: (newVal: number, oldVal: number) => void;
//   onActiveChange: (newVal: boolean, oldVal: boolean) => void;
// }

// 实战:类型安全的嵌套路径(点语法访问)
type DotPath<T, K extends keyof T = keyof T> =
  K extends string
    ? T[K] extends object
      ? K | `${K}.${DotPath<T[K]>}`
      : K
    : never;

type Config = { server: { host: string; port: number }; debug: boolean };
type ConfigPath = DotPath<Config>;
// 'server' | 'debug' | 'server.host' | 'server.port'
高级类型的实际价值

这些高级类型特性不是「炫技」——它们在大型项目中有实际价值:映射类型让 API 类型自动衍生(如从数据库模型生成 HTTP 请求/响应类型),模板字面量类型让路由系统和表单字段名称类型安全,条件类型让工具函数的返回类型精确匹配输入类型。掌握这些工具,可以消除大量手写类型标注的冗余工作。

组合应用:实战工具类型

真正的威力来自将映射类型、条件类型、infer 和模板字面量类型组合使用:

// DeepReadonly:递归将所有嵌套属性变为只读
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? DeepReadonly<T[K]>  // 递归处理嵌套对象
    : T[K];                // 基础类型不变
};

// DeepPartial:递归将所有嵌套属性变为可选
type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

// ValueOf:提取对象类型的所有值类型组成联合类型
type ValueOf<T> = T[keyof T];
const STATUS = { ACTIVE: 'active', INACTIVE: 'inactive' } as const;
type StatusValue = ValueOf<typeof STATUS>;  // 'active' | 'inactive'

// FunctionProperties:提取对象中所有函数类型的属性
type FunctionPropertyNames<T> = {
  [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];  // 末尾 [keyof T] 是索引访问,将对象类型的所有值联合起来

type Service = {
  name: string;
  start(): void;
  stop(): void;
  port: number;
};
type ServiceMethods = FunctionPropertyNames<Service>;  // 'start' | 'stop'

本章小结

本章核心要点