跳到主要内容

第 5 章 · 泛型

读完本章你会
  • 理解泛型解决的「重复代码」问题:把「类型」也当成参数
  • 编写泛型函数、泛型接口、泛型类
  • extends 约束类型参数,用默认类型参数兜底
  • 理解泛型推断:调用时 TS 自动填充类型参数
  • 用可变元组 [...T, U] 和 rest 参数写出更精确的类型

5.1 为什么需要泛型

想象你要写「取出数组第一项」,不想要运行时崩溃。如果逐个类型写:

function firstString(arr: string[]): string | undefined {
return arr[0]
}
function firstNumber(arr: number[]): number | undefined {
return arr[0]
}
// 每种类型抄一遍……

抄类型 = 维护噩梦。换个思路:把「元素类型」本身也当成参数,用的时候再定:

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

const s = first(['a', 'b']) // T 推断为 string → string | undefined
const n = first([1, 2, 3]) // T 推断为 number → number | undefined

T 就是一个类型参数:函数声明时不关心它具体是谁,调用时 TS 根据实参自动推断。这就是泛型——把「类型」变成可复用、可参数化的东西

泛型到底解决了什么

没有泛型时,你要么抄代码(每种类型一份),要么退化成 any(失去类型检查)。泛型让你既通用、又不丢失类型信息——这是它成为 TS 核心能力的原因。


5.2 泛型函数的基础

5.2.1 尖括号声明类型参数

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

T 是占位:调用时可以是任意类型,且「入参类型 = 出参类型」的关系被保持:

const a = identity('hello') // string
const b = identity(42) // number

5.2.2 调用时怎么定 T

大多数时候不用写,TS 自动推断:

first(['x', 'y']) // T 自动 = string

也可以显式指定(场景:推断不出来、或想强制类型):

const result = first<string | number>(['a', 1])

5.2.3 多个类型参数

function zip<A, B>(a: A[], b: B[]): [A, B][] {
return a.map((x, i) => [x, b[i]])
}

const pairs = zip(['name', 'age'], ['ada', 30])
// 类型:['name' | 'age' 对应的 (string, string|number)[]... 实际是 [string, string|number][]]
类型参数命名

惯例用单个大写字母:T(Type)、UV,语义化时用有意义的词:TKeyTValueElement。没有强制,但保持一致。


5.3 类型参数约束:extends

裸的 T 是「任意类型」。想限制「必须是某类」,加约束:

// T 必须是有 length 属性的类型
function getLength<T extends { length: number }>(value: T): number {
return value.length
}

getLength('hello') // ✅ string 有 length
getLength([1, 2, 3]) // ✅ 数组有 length
getLength(42) // ❌ number 没有 length

5.3.1 约束让 T 拥有能力

没有约束时,T 是「黑盒」,你不能假设它有 length

function bad<T>(x: T): number {
return x.length // ❌ 类型 'T' 上不存在属性 'length'
}

加了约束后,TS 知道 T 至少是 { length: number }x.length 就合法了。

5.3.2 keyof 约束:限制「键」

配合 keyof(第 6 章主角之一,这里先见个面):

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

const user = { name: 'ada', age: 30 }
getProp(user, 'name') // ✅
getProp(user, 'age') // ✅
getProp(user, 'id') // ❌ 'id' 不是 user 的键

K extends keyof T 的意思是「K 必须是 T 的键之一」——把「传错键」这个 bug 消灭在编译期。

5.3.3 约束用于「借用内置工具类型」

约束常和 Pick / Omit 这类内置工具类型联用(第 6 章手写它们):

function pickName<T extends { name: string }>(obj: T): T['name'] {
return obj.name
}

5.4 默认类型参数

和函数默认参数类似,类型参数也能给默认值:

function createArray<T = string>(length: number): T[] {
return new Array(length)
}

const a = createArray(3) // string[](用默认值)
const b = createArray<number>(3) // number[](显式指定)
默认参数的位置

默认类型参数要放在最后<T, U = string> 合法,<T = string, U> 非法。因为「跳过前面的默认值、只给后面的」没法写。


5.5 泛型接口与泛型类

5.5.1 泛型接口

interface Box<T> {
value: T
}

const strBox: Box<string> = { value: 'hello' }
const numBox: Box<number> = { value: 42 }

泛型接口最经典的应用——容器 / 数据结构

interface Repository<T> {
find(id: number): T | undefined
save(item: T): void
list(): T[]
}

5.5.2 泛型类

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

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

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

const s = new Stack<number>()
s.push(1)
s.push('x') // ❌ 'x' 不是 number

5.6 可变元组与 rest 参数

这是本章最「类型体操」的部分:泛型 + 元组的展开,让类型跟着数据走。

5.6.1 在元组前/后追加类型

// 在元组末尾追加一个 U
type Push<T extends unknown[], U> = [...T, U]
type Result1 = Push<[1, 2], 3> // [1, 2, 3]

// 在元组开头插入一个 U
type Unshift<T extends unknown[], U> = [U, ...T]
type Result2 = Unshift<[1, 2], 0> // [0, 1, 2]

// 拼接两个元组
type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U]
type Result3 = Concat<[1], [2, 3]> // [1, 2, 3]

5.6.2 rest 参数拿到「全部实参」的类型

function join(...parts: string[]): string {
return parts.join('-')
}

泛型版:rest 参数推断成元组,元素类型精确到「每位的类型」:

function tuple<T extends unknown[]>(...args: T): T {
return args
}

const t = tuple('a', 1, true)
// t 的类型是 ['a', 1, true] —— 每个位置精确!不是 (string|number|boolean)[]

这是「可变元组 + 泛型推断」的威力:rest 参数的类型会被推断成精确元组。类型体操里到处是它的身影(第 6 章、第 14 章都会用到)。


本章小结

  • 泛型把「类型」参数化,避免抄代码、又不丢失类型信息
  • T 在声明时是占位,调用时由 TS 自动推断,或显式指定
  • extends 约束类型参数的能力;keyof 约束「键」
  • 默认类型参数放在最后
  • 泛型接口 / 泛型类定义可复用的容器与数据结构
  • 可变元组 [...T, U] + rest 参数推断,让类型精确到每个位置

本章练习

挑战题

题号题目难度考察点
00018获取元组长度easyT['length'] + 约束
00014第一个元素easy元组解构 + infer
00015最后一个元素mediumrest 匹配尾部
00533Concateasy可变元组展开
03057Pusheasy元组尾部追加
03060Unshifteasy元组头部插入

每道题点「开始挑战」在官方 Playground(TS 6.0.3)作答。这 6 道题本质就是 5.6.1 的 Push / Unshift / Concat + 约束推断——做完你就把本章吃透了。

代码练习

  1. 泛型函数:写 last(arr) 返回数组最后一项(T | undefined),不写任何具体类型,验证传入 number[]string[] 时返回类型正确。

  2. 约束:写 sumBy<T extends { amount: number }>(items: T[]) 求和 amount 字段。传入不含 amount 的对象应报错。

  3. keyof 约束:实现 getProp(obj, key)(返回 T[K]),并验证传错键名会报错。

  4. 可变元组:用 rest 参数写 tail(arr),返回「去掉第一项后的元组」(提示:T extends [unknown, ...infer R] ? R : never),验证 tail([1, 2, 3]) 的类型是 [2, 3]

  5. 泛型类:实现一个 Queue<T>(入队 enqueue、出队 dequeue、判空 isEmpty),验证 new Queue<number>()enqueue('x') 报错。