泛型约束

默认情况下,泛型可以接受任意类型。有时我们需要限制泛型的范围,确保类型参数满足特定条件。泛型约束让我们可以精确控制类型参数的范围。

基本约束

使用 extends 关键字约束类型参数。

interface Lengthwise {
  length: number
}

function logLength<T extends Lengthwise>(arg: T): number {
  return arg.length
}

console.log(logLength("hello"))
console.log(logLength([1, 2, 3]))
console.log(logLength({ length: 10 }))

T extends Lengthwise 表示 T 必须有 length 属性。这让我们可以在函数内部安全地访问 length

约束类型

约束可以是接口、类型别名或原始类型。

function process<T extends string | number>(value: T): T {
  console.log(`处理值: ${value}`)
  return value
}

process("hello")
process(123)

T extends string | number 限制 T 只能是字符串或数字。

在约束中使用类型参数

可以在约束中使用另一个类型参数。

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

const user = {
  id: 1,
  name: "张三",
  email: "zhangsan@example.com"
}

console.log(getProperty(user, "name"))
console.log(getProperty(user, "email"))

K extends keyof T 确保 keyT 的有效属性名。keyof T 获取 T 的所有键的联合类型。

约束与默认类型

约束可以与默认类型结合使用。

interface ApiResponse<T extends object = Record<string, any>> {
  code: number
  message: string
  data: T
}

const response1: ApiResponse = {
  code: 200,
  message: "success",
  data: { name: "张三" }
}

const response2: ApiResponse<{ id: number; name: string }> = {
  code: 200,
  message: "success",
  data: { id: 1, name: "李四" }
}

T extends object = Record<string, any> 约束 T 必须是对象类型,默认值是 Record<string, any>

多重约束

可以使用交叉类型实现多重约束。

interface Named {
  name: string
}

interface Aged {
  age: number
}

function greet<T extends Named & Aged>(person: T): string {
  return `你好,我是 ${person.name},今年 ${person.age} 岁`
}

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

console.log(greet(person))

T extends Named & Aged 要求 T 同时满足 NamedAged 两个接口。

约束与类类型

可以使用类类型作为约束。

class Animal {
  name: string

  constructor(name: string) {
    this.name = name
  }
}

function createInstance<T extends Animal>(c: new (...args: any[]) => T, name: string): T {
  return new c(name)
}

const animal = createInstance(Animal, "小动物")
console.log(animal.name)

new (...args: any[]) => T 表示一个构造函数类型,返回 T 类型的实例。

条件约束

可以使用条件类型实现更复杂的约束。

type NonNullable<T> = T extends null | undefined ? never : T

function process<T extends NonNullable<T>>(value: T): T {
  return value
}

process("hello")
process(123)
process(true)

NonNullable<T> 排除了 nullundefined 类型。

约束数组元素

可以约束数组元素的类型。

function sum<T extends number>(numbers: T[]): number {
  return numbers.reduce((total, n) => total + n, 0)
}

console.log(sum([1, 2, 3]))
console.log(sum([1.5, 2.5, 3.5]))

T extends number 确保数组元素是数字类型。

约束与泛型函数

约束在泛型函数中特别有用。

function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 }
}

const result = merge(
  { name: "张三" },
  { age: 25 }
)

console.log(result.name)
console.log(result.age)

T extends object 确保参数是对象类型,而不是原始类型。

实际应用

泛型约束在实际开发中常用于确保类型安全。

interface Entity {
  id: number
}

class Repository<T extends Entity> {
  private items: T[] = []

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

  findById(id: number): T | undefined {
    return this.items.find(item => item.id === id)
  }

  update(item: T): boolean {
    const index = this.items.findIndex(i => i.id === item.id)
    if (index >= 0) {
      this.items[index] = item
      return true
    }
    return false
  }

  delete(id: number): boolean {
    const index = this.items.findIndex(item => item.id === id)
    if (index >= 0) {
      this.items.splice(index, 1)
      return true
    }
    return false
  }

  findAll(): T[] {
    return [...this.items]
  }
}

interface User extends Entity {
  name: string
  email: string
}

const userRepo = new Repository<User>()
userRepo.add({ id: 1, name: "张三", email: "zhangsan@example.com" })
userRepo.add({ id: 2, name: "李四", email: "lisi@example.com" })

console.log(userRepo.findById(1))
console.log(userRepo.findAll())

T extends Entity 确保 Repository 只能处理有 id 属性的实体类型。

小结

泛型约束是控制类型参数范围的重要工具。使用 extends 关键字可以约束类型参数满足特定条件。约束可以是接口、类型别名或原始类型。可以在约束中使用其他类型参数,实现更精确的类型控制。合理使用泛型约束可以提高代码的类型安全性和可维护性。