首页 / TypeScript 入门教程 / 泛型函数

TypeScript 入门教程

泛型函数

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

TypeScriptTypeScript 入门教程泛型泛型函数类型参数identity

本节目标:学会用泛型写出能处理多种类型但不丢失类型信息的函数,掌握类型参数推断、泛型约束和多泛型参数。

前面我们写的函数,参数和返回值类型都是固定的。但有时你需要一个函数”能处理多种类型”,而且”输入什么类型,输出就是什么类型”。any 可以做到前者,但做不到后者——它会丢掉类型信息。泛型就是来解决这个问题的。

类型参数:用 <T> 关联输入输出

先看一个最简单的例子——identity 函数(直接返回传入的参数):

// ❌ 用 any——丢失了类型信息
function identity(arg: any): any {
  return arg;
}

const result = identity("hello");
// result 的类型是 any,不是 string

// ✅ 用泛型——保留了类型信息
function identity<T>(arg: T): T {
  return arg;
}

const result = identity("hello");
// result 的类型是 string ✅
const num = identity(42);
// num 的类型是 number ✅

<T> 就是一个类型参数——它像一个类型变量,TypeScript 会根据你传入的实参自动推断 T 是什么。(arg: T): T 表达了一个关系:输入是 T,输出也是 T,两者绑定在一起。

泛型的核心思想就是这个:建立类型之间的对应关系。输入类型和输出类型不再独立,而是通过类型参数关联起来。

类型参数推断

大多数时候,你不需要手动指定类型参数——TypeScript 会从实参推断:

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

const s = firstElement(["a", "b", "c"]);  // s: string | undefined
const n = firstElement([1, 2, 3]);       // n: number | undefined

你看,调用 firstElement(["a", "b", "c"]) 时,TypeScript 从数组内容推断出 T = string,所以返回值是 string | undefined。整个过程你一句话没说。

但有些场景推断不出来,你就需要显式指定:

function combine<T>(arr1: T[], arr2: T[]): T[] {
  return arr1.concat(arr2);
}

// ❌ 推断失败:arr1 是 number[],arr2 是 string[],T 该是 number 还是 string?
combine([1, 2], ["a", "b"]); // 报错

// ✅ 显式指定 T 为联合类型
const result = combine<string | number>([1, 2], ["a", "b"]);
// result: (string | number)[]
Tip

类型参数的名字习惯用单个大写字母,如 T(Type)、UV。你也可以用更描述性的名字,比如 TItemTKey——在复杂场景下可读性更好。

泛型约束:用 extends 限制类型

泛型参数默认可以接受任意类型,所以函数体中你不能随便用 T 的属性:

function logLength<T>(arg: T): T {
  console.log(arg.length);
  //          ~~~~~~~~ ❌ 类型 "T" 上不存在属性 "length"
  return arg;
}

报错是因为 T 可以是任何类型,比如 number 就没有 .length。用 extends 加约束就能解决:

interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(arg: T): T {
  console.log(arg.length); // ✅ T 一定有 length 属性
  return arg;
}

logLength("hello");            // ✅ string 有 length
logLength([1, 2, 3]);          // ✅ 数组有 length
logLength({ length: 10 });    // ✅ 对象也有 length
logLength(42);                 // ❌ number 没有 length

T extends HasLength 的意思是:T 必须是 HasLength 的子类型——也就是说,传入的任何类型都必须有 length: number 这个属性。

一个常见的约束用法是配合 keyof

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const person = { name: "Alice", age: 30 };

getProperty(person, "name"); // ✅ 返回 string,"Alice"
getProperty(person, "age");  // ✅ 返回 number,30
getProperty(person, "email"); // ❌ "email" 不在 "name" | "age" 中

K extends keyof T 保证了第二个参数 key 只能是 T 的已有属性名——拼写错误会在编译期被拦下。

常见泛型模式

几个你在日常开发中最可能遇到的泛型模式:

identity(原样返回)

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

最简单的泛型,作用是”不丢失类型地传递一个值”。

数组 filter(保留元素类型)

function filterArray<T>(arr: T[], predicate: (item: T) => boolean): T[] {
  return arr.filter(predicate);
}

const numbers = [1, 2, 3, 4, 5];
const even = filterArray(numbers, (n) => n % 2 === 0);
// even: number[]——类型被保留了

map(输入和输出类型可以不同)

function mapArray<T, U>(arr: T[], transform: (item: T) => U): U[] {
  return arr.map(transform);
}

const strings = ["1", "2", "3"];
const parsed = mapArray(strings, (s) => parseInt(s));
// parsed: number[] ✅

这里用了两个类型参数——T 是输入数组的元素类型,U 是输出数组的元素类型。它们可以不同。

比较(泛型约束)

function longer<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

const longerArr = longer([1, 2], [1, 2, 3]);   // [1, 2, 3](number[])
const longerStr = longer("hi", "hello");         // "hello"("hi" | "hello")

longer 的返回类型跟传入的类型一致——传入 string[] 就返回 string[],传入两个字面量字符串就返回它们的联合类型。

写泛型函数的三个准则

泛型容易上瘾,但过度使用反而让代码更难读。记住三条准则:

1. 类型参数应该出现至少两次

如果一个类型参数只出现了一次,它没在关联任何东西——可能你根本不需要泛型。

// ❌ Str 只用了一次——不需要泛型
function greet<Str extends string>(s: Str) {
  console.log("Hello, " + s);
}

// ✅ 直接用 string
function greet(s: string) {
  console.log("Hello, " + s);
}

2. 尽量少用类型参数

多一个类型参数就多一层复杂度。尤其不要为无关的东西创建类型参数。

// ❌ Func 没有关联两个值——多余的泛型参数
function filterArr<T, Func extends (arg: T) => boolean>(
  arr: T[],
  func: Func
): T[] {
  return arr.filter(func);
}

// ✅ Func 不需要单独的类型参数
function filterArr<T>(arr: T[], func: (arg: T) => boolean): T[] {
  return arr.filter(func);
}

3. 能用类型参数本身,就别用约束

// ❌ 不必要地约束了 T
function firstEl<T extends any[]>(arr: T) {
  return arr[0];
}
// 返回值类型是 any

// ✅ 直接让 T 代表元素类型
function firstEl<T>(arr: T[]) {
  return arr[0];
}
// 返回值类型是 T

第二种写法不仅更简洁,返回类型也更精确(T 而不是 any)。

小结

泛型函数的核心是用类型参数建立输入输出之间的类型关系。<T> 定义类型参数,TypeScript 自动推断,extends 加约束。常见模式有 identity、array filter/map、以及带约束的比较函数。记住三准则:类型参数出现至少两次、越少越好、能用类型参数就别约束。