6Programming TypeScript

高级类型

类型体操

条件类型映射类型模板字面量类型infer

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第6章:高级类型(Advanced Types)

一、导读

1.1 本章学习目标

第6章讲解 TypeScript 的高级类型系统——条件类型、映射类型、模板字面量类型等。这些特性让你能够在类型层面进行"编程",创建强大的类型工具。

本章的核心学习目标包括:

  • 深入理解条件类型:T extends U ? X : Y
  • 掌握映射类型:如何基于已有类型创建新类型
  • 学会使用模板字面量类型:在类型层面操作字符串
  • 理解 `infer` 关键字:在条件类型中推断类型变量
  • 掌握递归条件类型

1.2 为什么本章如此重要

高级类型是 TypeScript 类型系统的"天花板"。掌握它们,你就能在类型层面实现复杂的逻辑——从 JSON 字符串推断出类型、从路由路径推断出参数类型、从 API 响应推断出数据结构。这些能力让 TypeScript 的类型系统成为一个强大的"类型级编程语言"。

1.3 核心问题引导

条件类型是如何工作的?什么是分布式条件类型?

infer 关键字的作用是什么?它能推断哪些位置的类型?

模板字面量类型能做什么?它和普通字符串字面量有什么区别?

递归条件类型有什么限制?如何避免无限递归?


二、核心概念详解

2.1 条件类型深入

typescript
// 基本形式
type IsArray<T> = T extends any[] ? true : false;

type A = IsArray<string[]>;    // true
type B = IsArray<string>;      // false

// 分布式条件类型:对联合类型的每个成员分别应用
type ToArray<T> = T extends any ? T[] : never;

type C = ToArray<string | number>;    // string[] | number[]

// 非分布式:用元组包裹
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;

type D = ToArrayNonDist<string | number>;    // (string | number)[]

2.2 infer 关键字

typescript
// 推断函数返回类型
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type R1 = GetReturnType<() => string>;    // string
type R2 = GetReturnType<(x: number) => boolean>;    // boolean

// 推断 Promise 的内部类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type P1 = UnwrapPromise<Promise<string>>;    // string
type P2 = UnwrapPromise<number>;             // number

// 推断构造函数参数
type ConstructorParams<T> = T extends new (...args: infer P) => any ? P : never;

class MyClass {
  constructor(public name: string, public age: number) {}
}
type Params = ConstructorParams<typeof MyClass>;    // [string, number]

2.3 映射类型

typescript
// 基本映射类型
type Readonly2<T> = { readonly [K in keyof T]: T[K] };
type Partial2<T> = { [K in keyof T]?: T[K] };

// 键重映射(key remapping)
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 }

// 过滤属性
type RemoveNull<T> = {
  [K in keyof T as T[K] extends null | undefined ? never : K]: T[K]
};

2.4 模板字面量类型

typescript
// 基本用法
type EventName = `on${Capitalize<string>}`;

// 联合类型组合
type Color = "red" | "blue" | "green";
type Size = "sm" | "md" | "lg";
type ButtonClass = `${Color}-${Size}`;
// "red-sm" | "red-md" | "red-lg" | "blue-sm" | ...

// 路由参数提取
type ExtractRouteParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractRouteParams<Rest>
    : T extends `${string}:${infer Param}`
      ? Param
      : never;

type Params1 = ExtractRouteParams<"/users/:id">;    // "id"
type Params2 = ExtractRouteParams<"/users/:id/posts/:postId">;    // "id" | "postId"

2.5 递归条件类型

typescript
// 深度 Readonly
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : DeepReadonly<T[K]>
    : T[K];
};

// 深度 Partial
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : DeepPartial<T[K]>
    : T[K];
};

// Flatten:展平嵌套数组
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;

type F1 = Flatten<number[][][]>;    // number

三、实战案例

3.1 案例一:类型安全的路由系统

typescript
type RouteMap = {
  "/users": { userId?: never };
  "/users/:id": { id: string };
  "/users/:id/posts/:postId": { id: string; postId: string };
  "/settings": {};
};

type ExtractParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof ExtractParams<Rest>]: string }
    : T extends `${string}:${infer Param}`
      ? { [K in Param]: string }
      : {};

type RouteParams<T extends keyof RouteMap> = ExtractParams<T>;

function navigate<T extends keyof RouteMap>(
  route: T,
  params: RouteParams<T>
) {
  let path = route as string;
  for (const [key, value] of Object.entries(params)) {
    path = path.replace(`:${key}`, value);
  }
  console.log(`Navigating to: ${path}`);
}

navigate("/users/:id", { id: "123" });
navigate("/users/:id/posts/:postId", { id: "123", postId: "456" });
navigate("/settings", {});

3.2 案例二:类型安全的 JSON 解析器

typescript
type JsonPrimitive = string | number | boolean | null;
type JsonObject = { [key: string]: JsonValue };
type JsonArray = JsonValue[];
type JsonValue = JsonPrimitive | JsonObject | JsonArray;

type JsonToType<T extends string> =
  T extends "null" ? null :
  T extends "true" ? true :
  T extends "false" ? false :
  T extends `${number}` ? number :
  T extends `"${infer S}"` ? S :
  T extends `[${infer Items}]` ? JsonArrayToType<Items> :
  T extends `{${infer Props}}` ? JsonObjectToType<Props> :
  never;

type Trim<S extends string> = S extends ` ${infer T}` ? Trim<T> :
  S extends `${infer T} ` ? Trim<T> : S;

四、本章小结

条件类型:T extends U ? X : Y,分布式条件类型对联合类型逐成员应用。

infer:在条件类型中推断类型变量,用于提取函数返回类型、Promise 内部类型等。

映射类型:{ [K in keyof T]: ... } 基于已有类型创建新类型,支持键重映射。

模板字面量类型:在类型层面操作字符串,实现路由参数提取等高级功能。

递归条件类型:实现 DeepReadonly、DeepPartial 等递归类型操作。


五、思考与练习

分布式条件类型和非分布式条件类型有什么区别?各适用于什么场景?

实现一个 DeepMutable<T> 工具类型,递归地移除所有 readonly 修饰符。

实现一个类型安全的模板引擎类型,从模板字符串中提取变量名。


6/7高级类型
0%