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


一、认识泛型
1,什么是泛型
(1)泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
(2)举个例子:封装一个函数,传入一个参数,并且返回这个参数。虽然该代码实现了功能,但是不适用于其他类型,如 string、boolean 等。有人可能会建议将 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,用于返回值类型
- O:Object 的缩写,表示对象
(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 接口中定义了 T1 和 T2 两个类型变量,T1 的默认类型为 string,T2 的默认类型为 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)有时我们希望传入的类型有某些共性,但是这些共性可能不在同一种类型中。例如,string 和 array 都有 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 中排除指定的属性:
提示:Pick 和 Omit 在处理 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)