返回 导航

其他

hangge.com

TypeScript - 泛型的使用详解(泛型函数、泛型接口、泛型类、约束、工具类型)

作者:hangge | 2026-08-13 08:45
        在软件工程中,构建明确和一致的 API 且具有可复用性是非常重要的。通过传入不同的函数参数,函数可以帮助我们完成不同的操作。但是,是否可以将参数的类型也进行参数化呢?答案是肯定的。将参数的类型进行参数化,这就是通常所说的类型参数化,也称为泛型(Generic)。本文将详细介绍泛型的定义、使用方式以及在函数、接口、类中的应用。

一、认识泛型

1,什么是泛型

(1)泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

(2)举个例子:封装一个函数,传入一个参数,并且返回这个参数。虽然该代码实现了功能,但是不适用于其他类型,如 stringboolean 等。有人可能会建议将 number 类型改为 any 类型,但这样会丢失类型信息。
function foo(arg: number): number {
  return arg
}

(3)这时,我们需要使用一种特殊的变量:类型变量(Type Variable),用于声明类型。通过下面代码可以看到,使用 <Type> 语法在 foo 函数中定义了一个类型变量 Type,接着该类型变量用于声明 arg 变量的类型和函数返回值的类型,这就是泛型的定义。
// 使用类型变量 Type
function foo<Type>(arg: Type): Type {
  return arg
}

// 调用方式一:向类型变量 Type 传递具体的类型
foo<number>(20)        // Type 为 number 类型
foo<string>('abc')     // Type 为 string 类型

// 调用方式二:TypeScript 会自动推导出 Type 具体的类型
foo(50)               // 自动推导为 number
foo('abc')            // 自动推导为 string

2,多个类型变量

函数上可以定义多个类型变量:
function foo<T, E>(a1: T, a2: E) {
  console.log(a1, a2)
}

foo<number, string>(20, 'abc')

3,泛型命名规范

(1)单个字母命名约定:
  • T:表示 Type,最常用的泛型参数
  • K:表示 Key,用于键类型
  • V:表示 Value,用于值类型
  • E:表示 Element,用于元素类型
  • R:表示 Return,用于返回值类型
  • OObject 的缩写,表示对象

(2)语义化命名:当泛型参数较多或有特定含义时,建议使用更具语义的名称:
// 使用语义化命名
interface ApiResponse<TData> {
  code: number
  message: string
  data: TData
}

// 或
function getProperty<TObject, TKey extends keyof TObject>(obj: TObject, key: TKey) {
  return obj[key]
}

4,何时使用泛型

(1)适合使用泛型的场景:
  • 函数、接口或类需要处理多种类型的数据
  • 需要保持输入和输出的类型一致性
  • 需要创建可复用的组件或工具函数
  • 需要实现通用的数据结构(如栈、队列、链表)

(2)不适合使用泛型的场景:
注意:如果你无法很好地回答「你想用泛型来提供什么样的约束」,那么你可能误用了泛型。
// 泛型误用示例:泛型仅用于单个参数的位置
function foo<T>(arg: T): void {
  console.log(arg)
}

// 更好的写法:直接使用具体类型
function foo(arg: unknown): void {
  console.log(arg)
}

5,范型与 any 的区别

(1)使用 any 会丢失类型信息,而泛型可以保留类型信息:
// 使用 any:丢失类型信息
function identityAny(arg: any): any {
  return arg
}

const result1 = identityAny(123)  // result1 的类型是 any

// 使用泛型:保留类型信息
function identityGeneric<T>(arg: T): T {
  return arg
}

const result2 = identityGeneric(123)  // result2 的类型是 number

(2)泛型的优势在于可以在编译时捕获类型错误,同时保持代码的灵活性。
提示:优先使用泛型而非 any,可以在保持代码灵活性的同时获得更好的类型安全性。

二、泛型接口、泛型类

1,接口中使用泛型

(1)泛型的应用非常广泛,不仅可以在函数中使用,还可以在定义接口时使用。
提示:泛型接口非常适合用于定义通用数据结构,如 API 响应格式、配置对象等。

(2)下面代码中在 IPerson 接口中定义了 T1T2 两个类型变量,T1 的默认类型为 stringT2 的默认类型为 number
// 1. 定义接口,在接口中定义 T1 和 T2 两个类型变量,并且都有默认值
interface IPerson<T1 = string, T2 = number> {
  name: T1    // name 的类型是 T1,需要调用者决定
  age: T2     // age 的类型是 T2,需要调用者决定
}

// 2. 将 p1 和 p2 指定为 IPerson 类型
const p1: IPerson = {
  name: "hangge",
  age: 18
}

const p2: IPerson<string, number> = {
  name: "hangge",
  age: 18
}

2,类中使用泛型

(1)在定义类时也可以使用泛型。
提示:泛型类常用于数据结构(如栈、队列、链表)以及工具类(如缓存、存储)的实现。

(2)下面代码在创建 p1 对象时,TypeScript 会自动推导出 T 类型变量的具体类型。在创建 p2 对象时,向 T 类型变量传递了具体的 string 类型。
// 1. 在 Point 类上定义 T 类型变量
class Point<T> {
  x: T
  y: T
  z: T

  constructor(x: T, y: T, z: T) {
    this.x = x
    this.y = y
    this.z = z
  }
}

// 2. TypeScript 会自动推导 T 类型变量的具体类型
const p1 = new Point("1.33.2", "2.22.3", "4.22.1")

// 3. 向 Point 类的 T 类型变量传递具体的 string 类型
const p2 = new Point<string>("1.33.2", "2.22.3", "4.22.1")

const p3: Point<string> = new Point("1.33.2", "2.22.3", "4.22.1")

四、泛型约束

1,为什么需要泛型约束

(1)有时我们希望传入的类型有某些共性,但是这些共性可能不在同一种类型中。例如,stringarray 都有 length 属性,或者某些对象也会有 length 属性。

(2)如果直接使用泛型,在函数内部操作泛型变量的属性或方法时,由于事先不知道它是哪种类型,会编译报错。这时,需要使用泛型约束来定义类型。
function getLength<T>(arg: T): number {
  return arg.length  // 报错:Property 'length' does not exist on type 'T'
}

2,使用 extends 进行约束

(1)我们可以定义一个接口来描述约束条件,然后使用 extends 关键字来实现约束。
(2)下面代码中,我们在 getLength 函数中定义了 T 类型变量,并且通过 extends 关键字为该类型添加了约束,约束 T 类型必须包含 ILength 接口中定义的 length 属性。
注意:在调用 getLength 函数时,传入的参数类型必须包含 length 属性,否则编译时会报错。
// 1. 接口定义约束条件
interface ILength {
  length: number
}

// 2. 在 getLength 函数中定义 T 类型变量,并添加类型的约束
// T 类型必须包含 ILength 接口中定义的 length 属性
function getLength<T extends ILength>(arg: T): number {
  return arg.length
}

// 3. 泛型约束的使用
getLength("abc")                          // 自动推导为 string(string 有 length 属性)
getLength<string[]>(["abc", "cba"])       // 传入 string[] 类型(有 length 属性)
getLength<{ length: number }>({ length: 100 })  // 传入对象类型(有 length 属性)

// getLength<number>(1000)  // 报错:Type 'number' does not satisfy the constraint 'ILength'

3,类型参数之间的约束

(1)可以声明一个类型参数,且它被另一个类型参数所约束。
提示:这种约束方式在实现通用工具函数时非常有用,可以避免运行时错误。

(2)比如,我们想要用属性名从对象里获取这个属性,并且想要确保这个属性存在于对象上。下面代码中 K extends keyof T 表示 K 必须是 T 的键名之一,这样就能保证传入的 key 一定存在于对象上。
function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key]
}

let x = { a: 1, b: 2, c: 3, d: 4 }

getProperty(x, "a")  // ok
getProperty(x, "m")  // 报错:Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'

附:泛型工具类型

1,常用工具类型介绍

(1)Partial<T>:将类型的所有属性变为可选:
interface IUser {
  id: number
  name: string
  age: number
}

// Partial 将所有属性变为可选
type PartialUser = Partial<IUser>
// 等价于:
// type PartialUser = {
//   id?: number
//   name?: string
//   age?: number
// }

const user: PartialUser = { name: "hangge" }  // ok

(2)Required<T>:将类型的所有属性变为必选:
interface IConfig {
  id?: number
  name?: string
}

// Required 将所有属性变为必选
type RequiredConfig = Required<IConfig>
// 等价于:
// type RequiredConfig = {
//   id: number
//   name: string
// }

(3)Readonly<T>:将类型的所有属性变为只读:
interface IPoint {
  x: number
  y: number
}

// Readonly 将所有属性变为只读
type ReadonlyPoint = Readonly<IPoint>

const point: ReadonlyPoint = { x: 1, y: 2 }
// point.x = 10  // 报错:Cannot assign to 'x' because it is a read-only property

2,Pick 和 Omit

(1)Pick<T,K>:从类型 T 中选取指定的属性:
interface IUser {
  id: number
  name: string
  email: string
  password: string
}

// Pick 只保留指定的属性
type BasicUser = Pick<IUser, "id" | "name">
// 等价于:
// type BasicUser = {
//   id: number
//   name: string
// }

const user: BasicUser = { id: 1, name: "hangge" }

(2)Omit<T,K>:从类型 T 中排除指定的属性:
提示PickOmit 在处理 API 响应数据、表单数据时非常有用,可以灵活地组合类型。
interface IUser {
  id: number
  name: string
  email: string
  password: string
}

// Omit 排除指定的属性
type SafeUser = Omit<IUser, "password">
// 等价于:
// type SafeUser = {
//   id: number
//   name: string
//   email: string
// }

const user: SafeUser = { id: 1, name: "hangge", email: "hangge@hangge.com" }

3,Record 类型

Record<K,T>:构造一个键类型为 K、值类型为 T 的对象类型:
提示:Record 常用于定义字典、映射表等数据结构。
// Record 构造对象类型
type RoleMap = Record<string, string[]>
// 等价于:
// type RoleMap = {
//   [key: string]: string[]
// }

const roles: RoleMap = {
  admin: ["read", "write", "delete"],
  user: ["read", "write"]
}
评论

全部评论(0)

回到顶部