讲解

泛型(generics)解决「类型作为参数」的问题。设想一个返回数组首元素的函数:数组是 number[] 时返回 number,是 string[] 时返回 string——用 any 写会丢掉这层对应关系。泛型的写法 function first(arr: T[]): T 引入了类型参数 T:调用时编译器根据实参把 T 推断为具体类型,返回值随之精确。类型参数就是一个「在调用时才确定的类型变量」,命名惯例是 T、U、K、V 或有语义的名字(TItem)。

泛型可以用于函数、接口、类、类型别名。interface Box { value: T } 定义了一个「装任意东西的盒子」类型;class Stack 则让栈这种经典数据结构对任意元素类型保持类型安全。使用时可以显式传类型参数(first([1, 2])),更多时候让编译器从实参推断。

调用时的推断规则值得理解:编译器收集所有出现 T 的参数位置,反推 T 的值;推不出来时报错,这时要么给默认值(<T = string>),要么显式指定。多个类型参数各自独立推断,function merge<A, B>(a: A, b: B): [A, B] 一次调用就能固定两个类型。

示例

泛型函数:类型随实参走:

function first<T>(arr: T[]): T | undefined {
  return arr.length === 0 ? undefined : arr[0];
}

const n: number | undefined = first([10, 20, 30]);
const s: string | undefined = first(["a", "b"]);
console.log(n, s);

泛型接口与类型别名:

interface ApiResponse<T> {
  code: number;
  message: string;
  data: T;
}

type Book = { title: string; price: number };

const resp: ApiResponse<Book[]> = {
  code: 0,
  message: "ok",
  data: [{ title: "活着", price: 39 }],
};
console.log(`${resp.message}:${resp.data[0].title}`);

泛型类:

class Stack<T> {
  private items: T[] = [];

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

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

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

const stack = new Stack<string>();
stack.push("第一层");
stack.push("第二层");
console.log(`弹出 ${stack.pop()},剩余 ${stack.size}`);

常见坑

  • 用 any 代替泛型:function identity(x: any): any 编译能过,但输入输出的类型联系断了——泛型存在的意义就是保住这层联系。
  • 类型参数只出现一次:function f(x: T): void 里的 T 没有任何约束价值,通常说明该位置直接写具体类型或 unknown 更合适。
  • 显式传参与推断打架:first([1]) 必然报错;显式传参前先想清楚实参是否匹配,多数情况让编译器推断。
  • 在泛型函数里假设 T 的能力:T 是「任意类型」,不能对 T 的值调 toFixed——那需要约束,下一章就讲。

小结

泛型 = 类型的参数化,T 在调用时确定;可用于函数、接口、类、别名;优先让编译器推断。下一章讲泛型约束、默认类型参数,正式进入进阶。