跳转至

第 4 章:高级类型编程

学习时间:5 小时 | 难度:⭐⭐⭐⭐ 高级 | 前置知识:第 2-3 章,理解泛型基础


本章概览

本章是 TypeScript 类型体操的核心——掌握这些工具,你可以用类型系统表达几乎任何约束,构建零运行时开销的类型安全库。

学习目标:

  • 掌握泛型约束(extends)和默认类型参数
  • 理解条件类型(Conditional Types)及 infer 关键字
  • 掌握映射类型(Mapped Types)和同态映射
  • 运用模板字面量类型(Template Literal Types)
  • 理解协变与逆变(Covariance/Contravariance)
  • 实战:手写常见工具类型的实现

4.1 泛型进阶

TypeScript
// ── 泛型约束(extends) ──────────────────────────────
// 约束 T 必须具有 length 属性
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

longest("hello", "world");    // string(OK)
longest([1, 2], [3]);         // number[](OK)
// longest(1, 2);             // ❌ number 没有 length 属性

// 约束 K 必须是 T 的键
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]; // 100% 类型安全的属性访问
}

const user = { name: "Alice", age: 30, active: true };
getProperty(user, "name");   // 返回 string
getProperty(user, "age");    // 返回 number
// getProperty(user, "xxx"); // ❌ 错误:"xxx" 不是 user 的键

// ── 多个泛型参数 ────────────────────────────────────
function merge<T extends object, U extends object>(target: T, source: U): T & U {
  return { ...target, ...source };
}

const merged = merge({ name: "Alice" }, { role: "admin" });
// merged: { name: string } & { role: string }
console.log(merged.name, merged.role); // 完整类型支持

// ── 泛型默认类型参数 ─────────────────────────────────
interface ApiClient<TBase = string> {
  baseUrl: TBase;
  get<TResponse>(path: string): Promise<TResponse>;
  post<TBody, TResponse>(path: string, body: TBody): Promise<TResponse>;
}

// 不传 TBase 时默认为 string
const client: ApiClient = { baseUrl: "https://api.example.com", get: async () => ({}), post: async () => ({}) };

// ── 递归泛型:深层嵌套类型 ───────────────────────────
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

interface Config {
  server: { host: string; port: number };
  database: { url: string; pool: { min: number; max: number } };
}

const config: DeepReadonly<Config> = {
  server: { host: "localhost", port: 3000 },
  database: { url: "postgres://...", pool: { min: 2, max: 10 } },
};
// config.server.host = "x"; // ❌ 错误:深层只读

4.2 条件类型(Conditional Types)

条件类型让类型系统可以做"判断":T extends U ? TrueType : FalseType

TypeScript
// ── 基础条件类型 ─────────────────────────────────────
type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string>;  // "yes"
type B = IsString<number>;  // "no"
type C = IsString<"hello">; // "yes"(字面量类型也满足 extends string)

// ── 分布式条件类型(Distributive Conditional Types) ──
// 当 T 是裸类型参数时,条件类型会自动分发到联合类型的每个成员
type NonNullable<T> = T extends null | undefined ? never : T;
// NonNullable<string | null | number> = string | number(分别分发再合并)

type ToArray<T> = T extends unknown ? T[] : never;
// ToArray<string | number> = string[] | number[](分布式!)
// 如果不想分布式,用方括号包裹:
type ToArrayNonDistributive<T> = [T] extends [unknown] ? T[] : never;
// ToArrayNonDistributive<string | number> = (string | number)[]

// ── 实用工具类型手写 ─────────────────────────────────
// 提取函数类型(内置 ReturnType 的实现原理)
type MyReturnType<T extends (...args: unknown[]) => unknown> =
  T extends (...args: unknown[]) => infer R ? R : never;

// 提取 Promise 的泛型参数(内置 Awaited 的实现原理)
type MyAwaited<T> =
  T extends Promise<infer Inner>
    ? Inner extends Promise<unknown>
      ? MyAwaited<Inner> // 递归处理嵌套 Promise
      : Inner
    : T;

type P1 = MyAwaited<Promise<string>>;           // string
type P2 = MyAwaited<Promise<Promise<number>>>;  // number(递归展开!)

// 过滤联合类型中的函数类型属性
type FunctionKeys<T> = {
  [K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? K : never;
}[keyof T];

interface UserService {
  id: number;
  name: string;
  getProfile(): Promise<void>;
  updateProfile(data: Partial<{ name: string }>): Promise<void>;
}

type Methods = FunctionKeys<UserService>; // "getProfile" | "updateProfile"

4.3 infer 关键字

infer 是条件类型的超能力——在类型匹配时"捕获"类型:

TypeScript
// ── 提取数组元素类型 ─────────────────────────────────
type ElementType<T> = T extends (infer E)[] ? E : never;
type N = ElementType<number[]>;     // number
type S = ElementType<string[]>;     // string
type M = ElementType<(string | number)[]>; // string | number

// ── 提取函数参数类型 ─────────────────────────────────
type FirstParam<T extends (...args: any[]) => any> =
  T extends (first: infer F, ...rest: any[]) => any ? F : never;

type F = FirstParam<(name: string, age: number) => void>; // string

// ── 提取类构造函数参数 ───────────────────────────────
type ConstructorParams<T extends new (...args: any[]) => any> =
  T extends new (...args: infer P) => any ? P : never;

class HttpClient {
  constructor(baseUrl: string, timeout: number) {}
}

type Params = ConstructorParams<typeof HttpClient>; // [baseUrl: string, timeout: number]

// ── 路径字符串类型提取(模板字面量 + infer 组合)──────
type ExtractRouteParams<S extends string> =
  S extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractRouteParams<`/${Rest}`>
    : S extends `${string}:${infer Param}`
      ? Param
      : never;

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

// 实际应用:类型安全的路由参数
type RouteHandler<Path extends string> = (
  params: Record<ExtractRouteParams<Path>, string>
) => void;

const handler: RouteHandler<"/users/:userId/posts/:postId"> = (params) => {
  console.log(params.userId, params.postId); // 完整类型支持
  // console.log(params.xxx); // ❌ 错误:"xxx" 不在参数中
};

4.4 映射类型(Mapped Types)

TypeScript
// ── 语法:[K in keyof T] ─────────────────────────────
type Stringify<T> = {
  [K in keyof T]: string; // 把所有属性类型改为 string
};

// ── 添加/删除修饰符 ──────────────────────────────────
// + (默认): 添加 readonly 或 ?
// - : 删除 readonly 或 ?

type Mutable<T> = {
  -readonly [K in keyof T]: T[K]; // 删除所有 readonly
};

type Required<T> = {
  [K in keyof T]-?: T[K]; // 删除所有 ?(变为必填)
};

// ── 同态映射(Homomorphic Mapped Types)────────────────
// 保留原有修饰符,只改变类型
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

// K as 重映射(Key Remapping, TS 4.1+)
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

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

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

// 过滤属性(K as 结合条件类型)
type PickByValue<T, ValueType> = {
  [K in keyof T as T[K] extends ValueType ? K : never]: T[K];
};

interface Mixed {
  id: number;
  name: string;
  active: boolean;
  createdAt: Date;
}

type StringProps = PickByValue<Mixed, string>; // { name: string }
type NumberProps = PickByValue<Mixed, number>; // { id: number }

// ── 实战:构建不可变的深层对象 API ───────────────────
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

function mergeDeep<T>(target: DeepPartial<T>, source: DeepPartial<T>): DeepPartial<T> {
  const result = { ...target };
  for (const key in source) {
    if (typeof source[key] === "object" && source[key] !== null) {
      result[key] = mergeDeep(result[key] ?? {}, source[key]) as any;
    } else {
      result[key] = source[key];
    }
  }
  return result;
}

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

TypeScript
// ── 基础模板字面量 ────────────────────────────────────
type Greeting = `Hello, ${string}!`; // 匹配所有 "Hello, xxx!" 格式的字符串

type EventName = "click" | "focus" | "blur";
type EventHandler = `on${Capitalize<EventName>}`; // "onClick" | "onFocus" | "onBlur"

// ── 构建类型安全的 CSS 类名 ──────────────────────────
type Size = "sm" | "md" | "lg" | "xl";
type Color = "primary" | "secondary" | "danger";
type ButtonClass = `btn-${Color}-${Size}`;
// "btn-primary-sm" | "btn-primary-md" | "btn-secondary-sm" | ... 16 个组合

// ── 构建事件系统类型 ─────────────────────────────────
type EventMap = {
  click:      { x: number; y: number };
  keypress:   { key: string; code: number };
  resize:     { width: number; height: number };
};

type OnEventKeys = `on${Capitalize<keyof EventMap>}`; // "onClick" | "onKeypress" | "onResize"

// 类型安全的事件发射器
class TypedEventEmitter<TEvents extends Record<string, unknown>> {
  private listeners = new Map<string, Function[]>();

  on<K extends keyof TEvents & string>(
    event: K,
    listener: (data: TEvents[K]) => void
  ): this {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event)!.push(listener);
    return this;
  }

  emit<K extends keyof TEvents & string>(event: K, data: TEvents[K]): void {
    this.listeners.get(event)?.forEach((fn) => fn(data));
  }
}

const emitter = new TypedEventEmitter<{
  userLogin:  { userId: string; ip: string };
  userLogout: { userId: string };
  pageView:   { url: string; referrer: string };
}>();

emitter.on("userLogin", ({ userId, ip }) => {
  console.log(`User ${userId} logged in from ${ip}`); // 完整类型支持
});

// emitter.on("unknown", () => {}); // ❌ 错误:"unknown" 不在事件映射中
// emitter.emit("userLogin", { userId: "1" }); // ❌ 错误:缺少 ip 字段

4.6 协变与逆变

TypeScript
// 协变(Covariance):子类型可以赋值给父类型
// ← 输出位置(return type)是协变的

type Producer<T> = () => T;

const stringProducer: Producer<string> = () => "hello";
// Producer<string> 可以赋值给 Producer<string | number> 吗?
// YES:返回 string 的函数可以赋值给返回 string | number 的位置
// (因为 string 是 string | number 的子类型)

// 逆变(Contravariance):父类型可以赋值给子类型
// → 输入位置(parameter type)是逆变的

type Consumer<T> = (value: T) => void;

const numberConsumer: Consumer<number> = (n) => console.log(n * 2);
// Consumer<string | number> 可以赋值给 Consumer<string> 吗?
// YES:接受 string | number 的函数,肯定也能接受 string

// 实际例子
type Handler<E extends Event> = (event: E) => void;
// Handler<MouseEvent> extends Handler<Event>? 
// MouseEvent extends Event(MouseEvent 是子类型)
// 但 Handler 是逆变的:Handler<Event> extends Handler<MouseEvent>(反过来!)

// 为什么重要?函数参数类型检查
function processEvent(handler: (event: Event) => void) {
  handler(new MouseEvent("click")); // 传入 MouseEvent
}

// ✅ 接受 Event(父类型)的函数可以传进来
processEvent((e: Event) => console.log(e.type));

// ❌ 接受 MouseEvent(子类型)的函数不能传进来
// (它假设 e 有 MouseEvent 的方法,但可能收到其他 Event 类型的值)
// processEvent((e: MouseEvent) => e.clientX); // 不安全!

4.7 实战:手写常见工具类型

TypeScript
// 手写 Exclude<T, U>
type MyExclude<T, U> = T extends U ? never : T;
type E = MyExclude<"a" | "b" | "c", "b">; // "a" | "c"

// 手写 Extract<T, U>
type MyExtract<T, U> = T extends U ? T : never;
type X = MyExtract<string | number | boolean, string | boolean>; // string | boolean

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

// 手写 Omit<T, K>
type MyOmit<T, K extends keyof T> = MyPick<T, Exclude<keyof T, K>>;

// 手写 Parameters<T>
type MyParameters<T extends (...args: any[]) => any> =
  T extends (...args: infer P) => any ? P : never;

// 手写 ReturnType<T>
type MyReturnType2<T extends (...args: any[]) => any> =
  T extends (...args: any[]) => infer R ? R : any;

// 进阶:UnionToIntersection(将联合转交叉)
type UnionToIntersection<U> =
  (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never;

type I = UnionToIntersection<{ a: string } | { b: number } | { c: boolean }>;
// { a: string } & { b: number } & { c: boolean }

// 进阶:TupleToUnion
type TupleToUnion<T extends readonly unknown[]> = T[number];
type TU = TupleToUnion<["a", "b", "c"]>; // "a" | "b" | "c"

// 进阶:Prettify(展开嵌套类型,改善 IDE 显示)
type Prettify<T> = {
  [K in keyof T]: T[K];
} & {};

type Complex = { a: string } & { b: number } & { c: boolean };
type Simple = Prettify<Complex>; // { a: string; b: number; c: boolean }(展开显示)

🏋️ 本章练习

  1. 条件类型:实现 DeepRequired<T>,递归地将所有属性变为必填
  2. infer 实战:实现 UnpackPromise<T>,支持提取 Promise<Promise<Promise<string>>> 最深层类型
  3. 映射类型:实现 EventHandlers<T>,将 { click: MouseEvent; keydown: KeyboardEvent } 转换为 { onClick: (e: MouseEvent) => void; onKeydown: (e: KeyboardEvent) => void }

TypeScript 5.8 · 类型编程指南 · 2025