第 6 章

泛型深度:约束、默认值、类型安全 fetch

第6章:泛型深度:约束、默认值、类型安全 fetch

泛型的本质是什么?这是理解本章内容的出发点。

本章核心问题:泛型的本质是什么?它描述的是输入-输出之间的什么关系?

读完本章你将理解


Level 1 · 你需要知道的(1-3年经验)

<T> 是关系,不是占位符

初学者把泛型理解为"可以填任意类型的空格",这个理解在简单场景够用,但会在复杂场景出错。准确理解:<T> 描述的是调用方传入的类型与函数内部行为之间的关系

// 错误理解:"T 是任意类型,所以这个函数接受任何值"
function identity<T>(value: T): T {
  return value;
}

// 正确理解:调用时 T 被推断为具体类型,
// 返回值类型与参数类型保持一致
const s = identity("hello"); // T = string,返回 string
const n = identity(42);      // T = number,返回 number

// 这才是泛型的意义:约束"输入-输出"的关系
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}
const x = first([1, 2, 3]); // T = number,x: number | undefined
const y = first(["a", "b"]); // T = string,y: string | undefined

extends 约束

裸泛型 <T> 不知道 T 有什么属性,编译器无法允许你访问任何成员。extends 约束告诉编译器:T 至少具备某种结构。

// 反模式:T 没有约束,无法访问任何属性
function getLength<T>(value: T): number {
  return value.length; // Error: Property 'length' does not exist on type 'T'
}

// 正确:约束 T 必须有 length 属性
function getLength<T extends { length: number }>(value: T): number {
  return value.length;
}

getLength("hello");    // OK,string 有 length
getLength([1, 2, 3]);  // OK,array 有 length
getLength(42);         // Error:number 没有 length

<T extends keyof U> — 属性名约束

这是 TypeScript 最常用的约束模式,用于安全地访问对象属性:

// 不安全版本
function getProp(obj: object, key: string): unknown {
  return (obj as any)[key]; // 丢失类型,返回 unknown
}

// 类型安全版本
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]; // 返回类型精确为 T[K]
}

const user = { name: "Alice", age: 30, active: true };
const name = getProp(user, "name");   // 类型:string
const age  = getProp(user, "age");    // 类型:number
getProp(user, "email"); // Error:'email' 不是 user 的键

对象约束

// T extends object 保证 T 不是原始类型
function merge<T extends object, U extends object>(a: T, b: U): T & U {
  return { ...a, ...b };
}

const result = merge({ x: 1 }, { y: "hello" });
// result: { x: number; y: string }

merge(1, { y: 2 }); // Error:number 不满足 extends object

多类型参数

多个类型参数可以相互引用,形成更精确的约束关系:

// K, V 独立:简单的键值映射
function mapKeys<K extends string, V>(
  keys: K[],
  getValue: (key: K) => V
): Record<K, V> {
  return Object.fromEntries(keys.map(k => [k, getValue(k)])) as Record<K, V>;
}

const scores = mapKeys(["alice", "bob"], name => name.length);
// 类型:Record<"alice" | "bob", number>

// U extends T:第二个参数是第一个的子集
function pick<T, U extends keyof T>(obj: T, keys: U[]): Pick<T, U> {
  const result = {} as Pick<T, U>;
  keys.forEach(k => { result[k] = obj[k]; });
  return result;
}

const user = { id: 1, name: "Alice", email: "[email protected]", role: "admin" };
const safe = pick(user, ["id", "name"]); // { id: number; name: string }

泛型默认值:<T = string>

当调用方没有显式传入类型参数时,使用默认值:

// 没有默认值:调用方必须传类型参数,否则 T 被推断为 unknown
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

// 有默认值:省略时 T = string
interface ApiResponse<T = string> {
  data: T;
  status: number;
  message: string;
}

// 使用默认值
const r1: ApiResponse = { data: "OK", status: 200, message: "" };
// 显式覆盖
const r2: ApiResponse<{ id: number }> = { data: { id: 1 }, status: 200, message: "" };

// 函数泛型默认值
function createList<T = string>(): T[] {
  return [];
}

const strings = createList();         // T = string,类型:string[]
const numbers = createList<number>(); // 显式,类型:number[]

泛型接口与泛型类

泛型接口

// 泛型接口:描述通用容器
interface Repository<T> {
  findById(id: number): Promise<T | null>;
  findAll(): Promise<T[]>;
  save(entity: T): Promise<T>;
  delete(id: number): Promise<void>;
}

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

// 实现时绑定具体类型
class UserRepository implements Repository<User> {
  private users: User[] = [];

  async findById(id: number): Promise<User | null> {
    return this.users.find(u => u.id === id) ?? null;
  }

  async findAll(): Promise<User[]> {
    return [...this.users];
  }

  async save(user: User): Promise<User> {
    const idx = this.users.findIndex(u => u.id === user.id);
    if (idx >= 0) this.users[idx] = user;
    else this.users.push(user);
    return user;
  }

  async delete(id: number): Promise<void> {
    this.users = this.users.filter(u => u.id !== id);
  }
}

泛型类

// 泛型栈:类型安全的 LIFO 容器
class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  get size(): number {
    return this.items.length;
  }

  isEmpty(): boolean {
    return this.items.length === 0;
  }
}

const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
const top = numStack.pop(); // 类型:number | undefined

const strStack = new Stack<string>();
strStack.push("hello");
strStack.push(42); // Error:Argument of type 'number' is not assignable to 'string'

keyoftypeof 与泛型结合

// keyof 提取类型的键名联合
type UserKeys = keyof User; // "id" | "name" | "email"

// typeof 从值推断类型
const defaultConfig = {
  timeout: 5000,
  retries: 3,
  baseUrl: "https://api.example.com",
};

type Config = typeof defaultConfig;
// { timeout: number; retries: number; baseUrl: string }

// 结合泛型:运行时安全地获取对象属性
function getRequired<T, K extends keyof T>(
  obj: T,
  key: K,
  fallback: NonNullable<T[K]>
): NonNullable<T[K]> {
  const val = obj[key];
  return (val != null ? val : fallback) as NonNullable<T[K]>;
}

interface Config {
  timeout?: number;
  host?: string;
}

const cfg: Config = { timeout: 3000 };
const timeout = getRequired(cfg, "timeout", 5000); // number,不含 undefined
const host    = getRequired(cfg, "host", "localhost"); // string

Level 2 · 它是怎么运行的(3-5年经验)

主项目:逐步构建类型安全的 fetch 封装

第一步:最基础版本

async function api<T>(url: string, options?: RequestInit): Promise<T> {
  const res = await fetch(url, options);
  if (!res.ok) {
    throw new Error(`HTTP ${res.status}: ${res.statusText}`);
  }
  return res.json() as Promise<T>;
}

// 使用:调用方声明期望类型
interface Post {
  id: number;
  title: string;
  body: string;
}

const post = await api<Post>("/api/posts/1");
post.title; // 类型:string,有完整智能提示

第二步:加入结构化错误

class ApiError extends Error {
  constructor(
    public status: number,
    public statusText: string,
    public body: unknown
  ) {
    super(`HTTP ${status}: ${statusText}`);
    this.name = "ApiError";
  }
}

async function api<T>(url: string, options?: RequestInit): Promise<T> {
  const res = await fetch(url, options);
  if (!res.ok) {
    let body: unknown;
    try { body = await res.json(); } catch { body = null; }
    throw new ApiError(res.status, res.statusText, body);
  }
  return res.json() as Promise<T>;
}

第三步:加入 baseURL 和默认 headers

interface ApiClientOptions {
  baseUrl: string;
  headers?: Record<string, string>;
  timeout?: number;
}

function createApiClient(clientOptions: ApiClientOptions) {
  const { baseUrl, headers: defaultHeaders = {}, timeout = 10000 } = clientOptions;

  async function request<T>(
    path: string,
    options: RequestInit & { params?: Record<string, string> } = {}
  ): Promise<T> {
    const { params, headers: reqHeaders, ...fetchOptions } = options;

    // 拼接查询参数
    const url = new URL(path, baseUrl);
    if (params) {
      Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
    }

    // 超时控制
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeout);

    try {
      const res = await fetch(url.toString(), {
        ...fetchOptions,
        headers: { ...defaultHeaders, ...reqHeaders as Record<string, string> },
        signal: controller.signal,
      });

      if (!res.ok) {
        let body: unknown;
        try { body = await res.json(); } catch { body = null; }
        throw new ApiError(res.status, res.statusText, body);
      }

      return res.json() as Promise<T>;
    } finally {
      clearTimeout(timer);
    }
  }

  return {
    get<T>(path: string, params?: Record<string, string>): Promise<T> {
      return request<T>(path, { method: "GET", params });
    },
    post<T>(path: string, body: unknown): Promise<T> {
      return request<T>(path, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
    },
    put<T>(path: string, body: unknown): Promise<T> {
      return request<T>(path, {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
    },
    delete<T>(path: string): Promise<T> {
      return request<T>(path, { method: "DELETE" });
    },
  };
}

// 使用示例
const client = createApiClient({
  baseUrl: "https://api.example.com",
  headers: { Authorization: "Bearer token123" },
});

interface User { id: number; name: string; email: string; }
interface Post { id: number; title: string; userId: number; }

const user = await client.get<User>("/users/1");
const posts = await client.get<Post[]>("/posts", { userId: "1" });
const newUser = await client.post<User>("/users", { name: "Bob", email: "[email protected]" });

泛型约束用于 DOM 操作

// T extends HTMLElement:只接受 DOM 元素,不接受普通对象
function querySelector<T extends HTMLElement>(
  selector: string,
  container: Document | Element = document
): T {
  const el = container.querySelector<T>(selector);
  if (!el) throw new Error(`Element not found: ${selector}`);
  return el;
}

const input = querySelector<HTMLInputElement>("#email");
input.value; // 类型:string,HTMLInputElement 特有属性

const canvas = querySelector<HTMLCanvasElement>("#chart");
canvas.getContext("2d"); // 类型:CanvasRenderingContext2D | null

// 泛型约束 + 事件监听
function addListener<K extends keyof HTMLElementEventMap>(
  el: HTMLElement,
  type: K,
  handler: (ev: HTMLElementEventMap[K]) => void
): () => void {
  el.addEventListener(type, handler as EventListener);
  return () => el.removeEventListener(type, handler as EventListener);
}

const btn = querySelector<HTMLButtonElement>("#submit");
const removeListener = addListener(btn, "click", (ev) => {
  ev.preventDefault(); // ev 类型:MouseEvent,有完整类型提示
});

反模式:过度泛化

// 反模式:用泛型表达其实是固定几种选项的情况
function format<T extends "json" | "xml" | "csv">(data: unknown, type: T): string {
  // T 这里没有任何实际约束作用
  if (type === "json") return JSON.stringify(data);
  if (type === "xml") return `<data>${data}</data>`;
  return String(data);
}

// 正确:直接用联合类型,更简洁,意图更清晰
type FormatType = "json" | "xml" | "csv";
function format(data: unknown, type: FormatType): string {
  if (type === "json") return JSON.stringify(data);
  if (type === "xml") return `<data>${data}</data>`;
  return String(data);
}

// 反模式:把不相关的参数都塞进泛型
function processAll<A, B, C>(a: A, b: B, c: C): [A, B, C] {
  return [a, b, c]; // 这里 A/B/C 之间没有任何关系,没有意义
}

// 正确:只在"输入-输出存在类型关系"时使用泛型
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second]; // 调用方可以用返回值的精确类型
}

判断是否需要泛型的标准


总结

特性 语法 典型用途
基础泛型 <T> identity 函数、容器类型
对象约束 <T extends object> merge、deep clone
属性约束 <K extends keyof T> 安全属性访问
多参数约束 <T, U extends T> 子集关系
泛型默认值 <T = string> API 响应接口
keyof + 泛型 <T, K extends keyof T>(obj: T, key: K): T[K] 类型安全属性读取
DOM 泛型 <T extends HTMLElement> querySelector 封装

下一章:拆解 TypeScript 内置工具类型的源码——Partial、Pick、Omit、Record 的实现原理。

Level 3 · 规范怎么定义的(资深)

TypeScript 的泛型约束系统(<T extends U>)实现的是有界量化(bounded quantification)。T extends { length: number } 表示 T 是满足 { length: number } 结构的所有类型的集合。与 Java 的 <T extends Comparable<T>> 不同,TypeScript 的约束基于结构子类型,不需要目标类型显式声明实现了某个接口。泛型默认值(<T = string>)的求值时机是在调用方没有显式传入类型参数且编译器无法从参数推断时才使用,这与函数参数默认值的语义一致。

Level 4 · 边界与陷阱(所有人)

过度泛化反模式:如果 T 只出现在参数列表中一次、且与返回类型无关,泛型没有意义——直接用联合类型或 unknown。判断标准:参数和返回值之间是否有类型关系?

泛型约束 extends object 的必要性:不加约束的 <T> 包含原始类型,{ ...a, ...b } 展开原始类型会产生意外结果。需要合并对象时始终用 <T extends object>

DOM 泛型 querySelector<T> 的类型断言document.querySelector<HTMLInputElement>('#email') 返回 HTMLInputElement | null,必须处理 null 情况。


本章评分
4.7  / 5  (56 评分)

💬 留言讨论