前言

泛型是 TypeScript 类型系统的核心特性,让代码在保持类型安全的同时具备复用性。本篇会讲清楚:

  • 泛型基础用法
  • 泛型约束
  • infer 关键字
  • 常用工具类型的实现原理

一、泛型基础

1.1 为什么需要泛型

// 问题:any 丢失类型信息
function identity(value: any): any {
  return value
}
const result = identity('hello')  // result 类型是 any

// 泛型:保留类型信息
function identity<T>(value: T): T {
  return value
}
const result2 = identity('hello')  // result2 类型是 string

1.2 泛型函数

// 单类型参数
function first<T>(arr: T[]): T | undefined {
  return arr[0]
}

first([1, 2, 3])      // number
first(['a', 'b'])     // string

// 多类型参数
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second]
}

pair('hello', 42)  // [string, number]

1.3 泛型接口

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

interface User {
  id: number
  name: string
}

// 使用
const response: ApiResponse<User> = {
  code: 200,
  message: 'success',
  data: { id: 1, name: 'Alice' }
}

1.4 泛型类

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

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

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

const numberStack = new Stack<number>()
numberStack.push(1)
numberStack.push(2)
// numberStack.push('3')  // 错误:不能将 string 赋值给 number

二、泛型约束

2.1 extends 约束

// 约束 T 必须有 length 属性
interface HasLength {
  length: number
}

function getLength<T extends HasLength>(value: T): number {
  return value.length
}

getLength('hello')     // ✅ string 有 length
getLength([1, 2, 3])   // ✅ array 有 length
// getLength(123)       // ❌ number 没有 length

2.2 keyof 约束

// 约束 K 必须是 T 的键
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

const user = { name: 'Alice', age: 25 }
getProperty(user, 'name')  // ✅ string
getProperty(user, 'age')   // ✅ number
// getProperty(user, 'email')  // ❌ 'email' 不是 user 的键

2.3 多重约束

interface Printable {
  print(): void
}

interface Loggable {
  log(): void
}

// T 必须同时实现 Printable 和 Loggable
function process<T extends Printable & Loggable>(item: T): void {
  item.print()
  item.log()
}

三、infer 关键字

3.1 基本用法

infer 用于在条件类型中推断类型。

// 推断数组元素类型
type ElementType<T> = T extends (infer U)[] ? U : T

type A = ElementType<number[]>     // number
type B = ElementType<string[]>     // string
type C = ElementType<boolean>      // boolean

3.2 推断函数返回值

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never

type A = ReturnType<() => string>           // string
type B = ReturnType<(x: number) => boolean> // boolean

3.3 推断函数参数

type Parameters<T> = T extends (...args: infer P) => any ? P : never

type A = Parameters<(a: string, b: number) => void>  // [string, number]

3.4 推断 Promise 内部类型

type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T

type A = Awaited<Promise<string>>              // string
type B = Awaited<Promise<Promise<number>>>     // number

四、常用工具类型实现

4.1 Partial

// 将所有属性变为可选
type Partial<T> = {
  [P in keyof T]?: T[P]
}

interface User {
  id: number
  name: string
}

type PartialUser = Partial<User>
// { id?: number; name?: string }

4.2 Required

// 将所有属性变为必选
type Required<T> = {
  [P in keyof T]-?: T[P]
}

interface User {
  id?: number
  name?: string
}

type RequiredUser = Required<User>
// { id: number; name: string }

4.3 Readonly

// 将所有属性变为只读
type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}

type ReadonlyUser = Readonly<User>
// { readonly id: number; readonly name: string }

4.4 Pick

// 从 T 中选取指定属性 K
type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}

type UserName = Pick<User, 'name'>
// { name: string }

4.5 Omit

// 从 T 中排除指定属性 K
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>

type UserWithoutId = Omit<User, 'id'>
// { name: string }

4.6 Record

// 构造一个属性键为 K,属性值为 T 的类型
type Record<K extends keyof any, T> = {
  [P in K]: T
}

type UserRoles = Record<string, string[]>
// { [key: string]: string[] }

4.7 Exclude / Extract

// Exclude:从 T 中排除可赋值给 U 的类型
type Exclude<T, U> = T extends U ? never : T

type A = Exclude<'a' | 'b' | 'c', 'a'>  // 'b' | 'c'

// Extract:从 T 中提取可赋值给 U 的类型
type Extract<T, U> = T extends U ? T : never

type B = Extract<'a' | 'b' | 'c', 'a' | 'c'>  // 'a' | 'c'

五、泛型实战

5.1 泛型工厂函数

function createInstance<T>(Ctor: new () => T): T {
  return new Ctor()
}

class User {
  name = 'Alice'
}

const user = createInstance(User)  // User 类型

5.2 泛型约束 API 响应

interface ApiRequest<T> {
  url: string
  method: 'GET' | 'POST'
  data?: T
}

interface ApiResponse<T> {
  code: number
  data: T
}

async function request<T, R>(config: ApiRequest<T>): Promise<ApiResponse<R>> {
  const response = await fetch(config.url)
  return response.json()
}

// 使用
const users = await request<null, User[]>({
  url: '/api/users',
  method: 'GET'
})

5.3 泛型与 React 组件

// 泛型列表组件
interface ListProps<T> {
  items: T[]
  renderItem: (item: T) => React.ReactNode
  keyExtractor: (item: T) => string
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map(item => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  )
}

// 使用
<List
  items={users}
  renderItem={user => <span>{user.name}</span>}
  keyExtractor={user => user.id.toString()}
/>

六、面试聚焦

6.1 泛型 vs any

// any:丢失类型信息,运行时可能出错
function process(value: any): any {
  return value.toUpperCase()  // 运行时才知道是否有这个方法
}

// 泛型:保留类型信息,编译时检查
function process<T>(value: T): T {
  // return value.toUpperCase()  // ❌ 编译错误:T 没有 toUpperCase
  return value
}

// 泛型约束:限制类型范围
function process<T extends string>(value: T): T {
  return value.toUpperCase()  // ✅ T 被约束为 string
}

6.2 ReturnType 原理一句话

// ReturnType 的实现原理:
// 使用 infer 推断函数返回值类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never

// 解读:如果 T 是函数类型,推断返回值为 R,否则为 never

七、易混淆点

  1. 泛型 vs anyany 放弃类型检查,泛型保留类型信息,在使用时确定具体类型。
  2. infer 的位置infer 只能在条件类型的 extends 子句中使用,用于推断类型变量。
  3. 泛型约束extends 不是继承,是约束泛型参数必须满足某个条件。
  4. 工具类型是类型Partial<T>Pick<T, K> 等是类型运算,不是运行时操作。

八、思考与练习

1. 泛型和 any 的区别是什么?

解析:

  • any:放弃类型检查,运行时可能出错
  • 泛型:保留类型信息,编译时检查,使用时确定具体类型

2. infer 的作用是什么?只能在哪里使用?

解析:

  • infer 用于在条件类型中推断类型变量
  • 只能在 extends 子句中使用

3. 实现一个 DeepPartial 类型。

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}

4. PickOmit 的区别是什么?

解析:

  • Pick<T, K>:从 T 中选取指定属性 K
  • Omit<T, K>:从 T 中排除指定属性 K
type A = Pick<User, 'name'>      // { name: string }
type B = Omit<User, 'name'>      // { id: number }

5. 解释 Partial<T> 的实现原理。

解析:

type Partial<T> = {
  [P in keyof T]?: T[P]
}

// 解读:
// 1. keyof T:获取 T 的所有属性键
// 2. P in keyof T:遍历所有属性键
// 3. ?:将每个属性变为可选
// 4. T[P]:保持原属性类型

总结

  • 泛型让代码在保持类型安全的同时具备复用性
  • 泛型约束通过 extends 限制泛型参数的范围
  • infer 用于在条件类型中推断类型变量
  • 工具类型是类型运算,编译时完成,不影响运行时
  • 泛型 vs any:泛型保留类型信息,any 放弃类型检查

更多推荐