泛型编程:灵活且安全

目录

第 1 节 概述

上一篇我们学了类。类让你把行为和数据打包。但问题是——如果一段逻辑可以适用于多种类型,怎么既保持复用性,又保持类型安全?

比如一个函数取数组的第一个元素:

1
2
3
function first(arr: number[]): number | undefined {
  return arr[0]
}

这个函数只能处理 number[]。如果输入 string[],返回类型就不好办了——返回 number | undefined 不对,写两个函数又太啰嗦。

泛型就是解决这个问题的。它允许你定义一个"类型参数",调用时再确定具体类型。

第 2 节 泛型函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function first<T>(arr: T[]): T | undefined {
  return arr[0]
}

// 调用时指定类型
const num = first<number>([1, 2, 3])   // number | undefined
const str = first<string>(["a", "b"])  // string | undefined

// 也可以不写,TypeScript 会自动推断
const num = first([1, 2, 3])   // 推断为 number | undefined
const str = first(["a", "b"])  // 推断为 string | undefined

<T> 是类型参数,习惯用大写单字母命名:T(Type)、K(Key)、V(Value)、E(Element)。

1
2
3
4
5
6
7
// 交换两个变量的值
function swap<T, U>(pair: [T, U]): [U, T] {
  return [pair[1], pair[0]]
}

const swapped = swap(["hello", 42])
// swapped 的类型为 [number, string]

第 3 节 泛型约束

有时候你不希望 T 是"任何类型",而是"必须有某些属性"的类型。用 extends 添加约束:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
interface HasLength {
  length: number
}

// T 必须具有 length 属性
function logLength<T extends HasLength>(arg: T): T {
  console.log(arg.length)
  return arg
}

logLength("hello")      // ✅ string 有 length
logLength([1, 2, 3])    // ✅ array 有 length
logLength(42)           // ❌ number 没有 length

另一个常见模式——约束键的类型:

1
2
3
4
5
6
7
8
9
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

const person = { name: "张三", age: 18, email: "z@example.com" }

getProperty(person, "name")   // ✅ 返回 string
getProperty(person, "age")    // ✅ 返回 number
getProperty(person, "weight") // ❌ "weight" 不在 keyof T 中

第 4 节 泛型接口

接口也可以带类型参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
interface ApiResponse<T> {
  code: number
  message: string
  data: T
}

// 不同接口返回不同的 data 类型
type UserResponse = ApiResponse<{ id: number; name: string }>
type ListResponse = ApiResponse<string[]>

const userRes: UserResponse = {
  code: 200,
  message: "成功",
  data: { id: 1, name: "张三" },
}

泛型接口描述函数类型也很常见:

1
2
3
4
5
6
7
interface Callback<T> {
  (data: T): void
}

function fetchData<T>(url: string, cb: Callback<T>) {
  // ...
}

第 5 节 泛型类

类和接口一样,可以带有类型参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Box<T> {
  private content: T

  constructor(value: T) {
    this.content = value
  }

  getValue(): T {
    return this.content
  }

  setValue(value: T): void {
    this.content = value
  }
}

const numberBox = new Box<number>(42)
numberBox.getValue()  // number

const stringBox = new Box<string>("hello")
stringBox.getValue()  // string

第 6 节 多个泛型参数

需要两个独立类型时,可以用多个参数:

1
2
3
4
5
6
7
function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 }
}

const merged = merge({ name: "张三" }, { age: 18 })
// merged 的类型为 { name: string } & { age: number }
// 等价于 { name: string; age: number }

多个参数各自可以有约束:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function pair<T extends string | number, U extends string | number>(
  first: T,
  second: U,
): [T, U] {
  return [first, second]
}

pair(1, 2)        // ✅
pair("a", "b")    // ✅
pair(1, "b")      // ✅
pair(true, 1)     // ❌ boolean 不在约束中

第 7 节 泛型 vs any

泛型不是 any,两者有本质区别:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 使用 any —— 失去了类型安全
function firstAny(arr: any[]): any {
  return arr[0]
}
const x: string = firstAny([1, 2, 3])  // 不报错,但 x 实际上是 number,不安全

// 使用泛型 —— 保持类型关系
function firstGen<T>(arr: T[]): T | undefined {
  return arr[0]
}
const y: string = firstGen([1, 2, 3])  // ❌ 类型错误,T 被推断为 number
对比泛型any
输入与输出的关系保持精确对应切断联系
类型安全✅ 编译期检查❌ 无检查
使用场景逻辑通用但类型需关联逃生舱(极少用)

TypeScript 还内置了 PartialPickOmitRecord 等工具类型,进阶篇中会系统介绍。


泛型是 TypeScript 的"高级关卡"——学会了它,你的类型水平会上一个新台阶。

但泛型也带来一个新问题:当类型在运行时变得不确定时,怎么确保我们安全地使用它们?下一篇讲类型守卫——运行时安全的第一道防线。

目录