首页 / TypeScript 入门教程 / 泛型基础

TypeScript 入门教程

泛型基础

本教程共 80 篇 · 第 38 篇 · 更新于 2026-08-10 · 约 12 分钟阅读

TypeScriptTypeScript 入门教程泛型泛型类类型变量identity

本节目标:从”为什么要泛型”出发,掌握类型变量的概念,学会定义泛型函数类型和泛型接口,初步接触泛型类。

第 22 章我们写了泛型函数——你知道怎么用 <T> 让输入输出的类型绑定在一起。但泛型远不止函数这一件事。它是 TypeScript 类型系统里最核心的抽象工具之一,贯穿接口、类、类型别名,甚至后面要讲的条件类型和映射类型。这一章我们站在更高的地方,重新审视泛型的”地基”。

为什么需要泛型:any 做不到的事

假设你要写一个函数,它能接收一个数组,返回这个数组的第一个元素。不用泛型的话,你可以这样写:

function first(arr: any[]): any {
  return arr[0];
}

const val = first([1, 2, 3]);
// val 的类型是 any 😞

返回 any 的问题在于——你丢失了 type safety。valany,它可以赋给一个 string 变量而不报错,但运行时它其实是个 number。编译器眼睁睁看着你犯错,却什么都做不了。

另一个思路是为每种类型都写一个版本:

function firstNumber(arr: number[]): number { return arr[0]; }
function firstString(arr: string[]): string { return arr[0]; }
// ……以后每多一种类型,都要复制一份

代码重复到令人窒息。而且你很难提前知道未来会有哪些类型。

泛型就是来解决这个矛盾的——既能处理多种类型,又不会丢掉类型信息。

类型变量:用一个字母代替整个类型

泛型的核心概念是类型变量。它跟普通变量的逻辑一样,只不过普通变量存的是”值”,类型变量存的是”类型”。

// 值变量:存的是具体的数据
let value = "hello";

// 类型变量:存的是具体的类型
function identity<T>(arg: T): T {
  return arg;
}

<T> 声明了一个类型变量 T。调用 identity("hello") 时,TypeScript 推断 T = string,于是整个函数签名变成了 (arg: string): string。类型变量让签名不再是写死的,而是”模板化的”——你给什么类型,它就变成对应的签名。

你可以把 T 想象成一个占位符:写代码的时候你不知道它是什么,但 TypeScript 在调用处帮你”填”进去。

命名惯例

单一类型参数习惯叫 T,多个参数依次用 TUV。如果参数有明确语义,用描述性名称更好理解:

// 简单场景:T 就够了
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

// 多个参数时:TKey、TValue 比 T、U 更清晰
type Dictionary<TKey, TValue> = {
  [key: TKey]: TValue;
};

习惯上以 T 为前缀,因为泛型在其他语言里也叫 template。

泛型不仅仅属于函数

泛型可以附着在很多地方。用泛型函数的写法你已经会了:

function identity<T>(arg: T): T {
  return arg;
}

那把 identity 的类型单独抽出来会是什么?

泛型函数类型

跟普通函数类型写法类似,只不过前面多了 <T>

let myIdentity: <T>(arg: T) => T = identity;

// 类型参数的名字无所谓,叫 Input 也行
let myIdentity2: <Input>(arg: Input) => Input = identity;

关键点是:类型参数的位置和个数要对上,名字可以随便取。

如果用对象字面量形式的调用签名,也是一样的:

let myIdentity: { <T>(arg: T): T } = identity;

泛型接口

把上面那句的类型往外提到接口上:

interface GenericIdentityFn<T> {
  (arg: T): T;
}

function identity<T>(arg: T): T {
  return arg;
}

// 使用时需要指定 T 的具体类型
let stringIdentity: GenericIdentityFn<string> = identity;
// stringIdentity 现在是 (arg: string) => string

注意区分两种写法的差别:

  • 类型参数在调用签名上<T>(arg: T): T):这个接口描述的是一个泛型函数本身。
  • 类型参数在接口上GenericIdentityFn<T>):接口本身是泛型的,使用时锁定类型。

这两种写法对应不同的需求。前一种适合泛型回调函数,后一种适合你想在类型层面就绑定好特定类型的场景。

泛型类:一个队列的例子

泛型类就是在类名后面加 <T>,让类的整个实例共享同一个类型参数。经典例子:实现一个泛型栈(Stack)。

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;
  }
}

// 数字栈
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
// numberStack.push("3"); // ❌ 类型错误

// 字符串栈
const stringStack = new Stack<string>();
stringStack.push("hello");

// 从中取出的元素类型自动是 T
const num = numberStack.pop();  // number | undefined
const str = stringStack.pop();  // string | undefined

一个 Stack<T> 管所有类型,不用为每种类型重复写一遍代码。而且类型安全:推入数字的栈,弹出的也一定是数字。

Note

泛型类的静态成员不能使用类的类型参数。因为静态成员属于类本身,不属于实例——它在所有实例之间共享,无法绑定到某一个具体的 T

类型参数推断

大多数时候你不需要显式写 <>,TypeScript 会从参数推断:

const stack = new Stack<number>();     // 显式指定
// 若 Stack 有接受数组的构造函数(如 constructor(initial: T[])),也可用 new Stack([1, 2, 3]) 自动推断 T = number

函数同理:

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

const num = first([1, 2, 3]);  // 推断 T = number,返回值 number | undefined
const str = first(["a", "b"]); // 推断 T = string,返回值 string | undefined

如果推断不出来或者你想强制覆盖,才需要显式写 <类型>

泛型不是银弹

泛型虽好,也别滥用。有些场景你不需要泛型,用联合类型就够了:

// 这种写法没必要——T 只用了一次,跟用 any 加断言区别不大
function log<T>(value: T): T {
  console.log(value);
  return value;
}

// 还不如直接用参数类型
function log(value: unknown): void {
  console.log(value);
}

泛型的价值在于它建立类型之间的关联。如果类型参数只在一个位置出现,泛型就退化成了”花哨的 any”,意义不大。

小结

泛型不是什么魔法——它就是让你用类型变量来建立类型之间的对应关系。一个 <T> 管输入输出,让函数、接口、类在保持类型安全的同时具备复用性。核心公式就一句话:类型变量是模板,调用时填入具体类型,TypeScript 帮你推导。 下一章我们聊聊怎么给类型变量加约束条件——让它不能接受”所有类型”,而只接受”满足某种条件的类型”。