TypeScript 泛型
大白话解释: 泛型就是"万能遥控器"。普通遥控器只能控制一个品牌的电视,万能遥控器不管什么品牌都能用。泛型让函数/类支持多种类型,不用为每种类型写一遍代码。
为什么要用泛型?
- 代码复用:写一次,支持多种类型
- 类型安全:编译时检查类型,不会出现运行时错误
- 灵活扩展:可以添加约束,限制泛型的范围
先理解问题:为什么需要泛型?
没有泛型的痛苦
ts
// 假设你要写一个"返回第一个元素"的函数
// 方案1:用 any(不推荐)
function firstAny(arr: any[]): any {
return arr[0]
}
// 问题:丢失了类型信息
const result1 = firstAny([1, 2, 3]) // any 类型,不知道是 number
// 方案2:为每种类型写一个函数(太累了)
function firstNumber(arr: number[]): number {
return arr[0]
}
function firstString(arr: string[]): string {
return arr[0]
}
function firstBoolean(arr: boolean[]): boolean {
return arr[0]
}
// 问题:代码重复,100种类型就要写100个函数
// 方案3:用泛型(推荐)
function first<T>(arr: T[]): T {
return arr[0]
}
// 完美:一个函数搞定所有类型,而且保留了类型信息
const num = first([1, 2, 3]) // number 类型
const str = first(['a', 'b', 'c']) // string 类型泛型基础
泛型函数
ts
// ---- 泛型函数:让函数支持多种类型 ----
// 大白话:T 就像一个"占位符",调用时才确定具体是什么类型
// 基本泛型函数
function identity<T>(arg: T): T {
return arg
}
// 解读:
// <T>:声明一个类型参数 T
// arg: T:参数的类型是 T
// : T:返回值的类型也是 T
// 调用时 T 会被替换成实际类型
// 使用方式1:手动指定类型(不常用)
const num = identity<number>(42) // T = number
const str = identity<string>('hello') // T = string
// 使用方式2:让 TS 自动推断(推荐)
const num2 = identity(42) // TS 自动推断 T = number
const str2 = identity('hello') // TS 自动推断 T = string
// ---- 多个泛型参数 ----
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second]
}
// T 和 U 是两个不同的类型参数,可以不同
const p1 = pair('hello', 42) // [string, number]
const p2 = pair(42, true) // [number, boolean]
const p3 = pair('a', 'b') // [string, string]
// ---- 泛型约束:限制 T 必须满足某些条件 ----
interface HasLength {
length: number
}
// T 必须有 length 属性(用 extends 关键字)
function logLength<T extends HasLength>(arg: T): T {
console.log(arg.length) // 安全,因为 T 一定有 length
return arg
}
logLength('hello') // ✅ string 有 length(5)
logLength([1, 2, 3]) // ✅ array 有 length(3)
logLength({ length: 5 }) // ✅ 有 length 属性
// logLength(42) // ❌ number 没有 length
// logLength(true) // ❌ boolean 没有 length泛型接口
ts
// ---- 泛型接口:让接口支持不同类型 ----
// 大白话:就像"快递箱",箱子的形状固定,但里面装的东西可以是任意类型
// 泛型接口
interface ApiResponse<T> {
code: number
message: string
data: T // T 是占位符,使用时才确定
}
// 使用:指定 T 为 User
interface User {
id: number
name: string
}
const response: ApiResponse<User> = {
code: 200,
message: 'success',
data: { id: 1, name: '张三' }, // data 的类型是 User
}
// 使用:指定 T 为 User[]
const listResponse: ApiResponse<User[]> = {
code: 200,
message: 'success',
data: [{ id: 1, name: '张三' }], // data 的类型是 User[]
}
// ---- 泛型接口的实际应用 ----
// 列表响应
interface ListResponse<T> {
items: T[]
total: number
page: number
pageSize: number
}
// 继承泛型接口
interface UserListResponse extends ListResponse<User> {
// 继承后,items 自动变成 User[]
}
const userList: UserListResponse = {
items: [{ id: 1, name: '张三' }],
total: 100,
page: 1,
pageSize: 10,
}
// 泛型函数接口
interface Factory<T> {
create(): T
validate(item: T): boolean
}
class UserFactory implements Factory<User> {
create(): User {
return { id: Date.now(), name: '' }
}
validate(user: User): boolean {
return user.name.length > 0
}
}泛型类
ts
// ---- 泛型类:让类支持不同类型 ----
// 大白话:就像"栈"这个数据结构,不管存数字还是字符串,操作方式都一样
class Stack<T> {
private items: T[] = []
push(item: T): void {
this.items.push(item)
}
pop(): T | undefined {
return this.items.pop()
}
peek(): T | undefined {
return this.items[this.items.length - 1]
}
isEmpty(): boolean {
return this.items.length === 0
}
size(): number {
return this.items.length
}
}
// 使用:存数字
const numberStack = new Stack<number>()
numberStack.push(1)
numberStack.push(2)
numberStack.push(3)
console.log(numberStack.pop()) // 3
// numberStack.push('hello') // ❌ 错误,不能存 string
// 使用:存字符串
const stringStack = new Stack<string>()
stringStack.push('hello')
stringStack.push('world')
console.log(stringStack.peek()) // 'world'
// ---- 实际应用:通用队列 ----
class Queue<T> {
private items: T[] = []
enqueue(item: T): void {
this.items.push(item)
}
dequeue(): T | undefined {
return this.items.shift()
}
front(): T | undefined {
return this.items[0]
}
isEmpty(): boolean {
return this.items.length === 0
}
}
const queue = new Queue<string>()
queue.enqueue('任务1')
queue.enqueue('任务2')
queue.enqueue('任务3')
console.log(queue.dequeue()) // '任务1'泛型约束
extends 约束
ts
// ---- extends 约束:限制泛型必须满足某些条件 ----
// 大白话:就像"入场券",必须满足条件才能进来
// 约束 T 必须有 length 属性
interface HasLength {
length: number
}
function logLength<T extends HasLength>(arg: T): T {
console.log(arg.length)
return arg
}
// 约束 T 必须是某个类的实例
class Animal {
name: string
constructor(name: string) {
this.name = name
}
}
class Dog extends Animal {
bark() {
console.log('汪汪叫')
}
}
// T 必须是 Animal 或其子类
function createAnimal<T extends Animal>(ctor: new (name: string) => T, name: string): T {
return new ctor(name)
}
const dog = createAnimal(Dog, '旺财') // Dog 类型
dog.bark() // 汪汪叫
// 约束 T 必须是对象类型
function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 }
}
const merged = merge({ name: '张三' }, { age: 25 })
// { name: string; age: number }keyof 约束
ts
// ---- keyof 约束:限制 key 必须是对象的属性 ----
// 大白话:就像"只能用钥匙开门",不能用别的东西
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: '张三', age: 25 }
const name = getProperty(user, 'name') // string 类型
const age = getProperty(user, 'age') // number 类型
// const email = getProperty(user, 'email') // ❌ 'email' 不是 user 的属性
// 解读:
// T:对象的类型
// K extends keyof T:K 必须是 T 的某个属性名
// T[K]:返回值的类型就是 T 中 K 对应的类型
// ---- 实际应用:安全地设置对象属性 ----
function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
obj[key] = value
}
setProperty(user, 'name', '李四') // ✅
// setProperty(user, 'name', 123) // ❌ 类型不匹配
// setProperty(user, 'email', 'xxx') // ❌ 'email' 不存在泛型默认值
ts
// ---- 泛型默认值:给泛型参数一个默认类型 ----
// 大白话:就像"默认选项",不指定就用默认的
interface ApiResponse<T = unknown> {
code: number
message: string
data: T
}
// 使用默认类型(T = unknown)
const response1: ApiResponse = {
code: 200,
message: 'success',
data: 'some data', // data 类型为 unknown
}
// 指定类型(T = User)
const response2: ApiResponse<User> = {
code: 200,
message: 'success',
data: { id: 1, name: '张三' }, // data 类型为 User
}
// ---- 多个泛型参数 ----
interface EventHandler<T = Event, U = void> {
(event: T): U
}
// 使用默认值(T = Event, U = void)
const clickHandler: EventHandler = (event) => {
console.log(event.target)
}
// 只指定第一个参数(T = KeyboardEvent, U = void)
const keyHandler: EventHandler<KeyboardEvent> = (event) => {
console.log(event.key)
}
// 指定两个参数(T = SubmitEvent, U = boolean)
const submitHandler: EventHandler<SubmitEvent, boolean> = (event) => {
event.preventDefault()
return true
}泛型工具类型
更多工具类型详见 TypeScript 类型系统
ts
// ---- 内置工具类型:TS 帮你写好的"类型函数" ----
// 大白话:就像"瑞士军刀",常用的类型变换工具都有了
interface User {
id: number
name: string
email?: string
}
// Partial<T>:所有属性变为可选
type PartialUser = Partial<User>
// { id?: number; name?: string; email?: string }
// Required<T>:所有属性变为必需
type RequiredUser = Required<User>
// { id: number; name: string; email: string }
// Readonly<T>:所有属性变为只读
type ReadonlyUser = Readonly<User>
// { readonly id: number; readonly name: string; readonly email?: string }
// Pick<T, K>:选取部分属性
type UserBasic = Pick<User, 'id' | 'name'>
// { id: number; name: string }
// Omit<T, K>:排除部分属性
type UserWithoutEmail = Omit<User, 'email'>
// { id: number; name: string }
// Record<K, V>:构造键值对类型
type UserMap = Record<string, User>
// { [key: string]: User }高级泛型模式
条件类型
ts
// ---- 条件类型:根据条件决定类型 ----
// 大白话:就像"三元运算符",但用在类型上
// 语法:T extends U ? X : Y(如果 T 是 U 的子类型,就是 X,否则是 Y)
type IsString<T> = T extends string ? true : false
type A = IsString<string> // true
type B = IsString<number> // false
// 实际应用:过滤类型
type Exclude<T, U> = T extends U ? never : T
type Extract<T, U> = T extends U ? T : never
type Status = 'active' | 'inactive' | 'pending'
type ActiveStatus = Exclude<Status, 'inactive' | 'pending'> // 'active'
type InactiveStatus = Extract<Status, 'inactive' | 'pending'> // 'inactive' | 'pending'
// 分布式条件类型:联合类型的每个成员都应用条件
type ToArray<T> = T extends any ? T[] : never
type C = ToArray<string | number> // string[] | number[]
// 等价于 ToArray<string> | ToArray<number> = string[] | number[]infer 关键字
ts
// ---- infer:在条件类型中"捕获"类型 ----
// 大白话:就像"抓娃娃机",从类型中抓出你想要的部分
// 提取函数返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never
// 解读:如果 T 是函数类型,就"抓出"返回值类型 R,否则是 never
type D = ReturnType<() => string> // string
type E = ReturnType<(x: number) => boolean> // boolean
// 提取函数参数类型
type Parameters<T> = T extends (...args: infer P) => any ? P : never
// 解读:如果 T 是函数类型,就"抓出"参数类型 P
type F = Parameters<(a: string, b: number) => void> // [a: string, b: number]
// 提取数组元素类型
type ElementType<T> = T extends (infer E)[] ? E : never
// 解读:如果 T 是数组类型,就"抓出"元素类型 E
type G = ElementType<string[]> // string
type H = ElementType<number[]> // number
// 提取 Promise 的值类型
type Awaited<T> = T extends Promise<infer U> ? U : T
type I = Awaited<Promise<string>> // string
type J = Awaited<Promise<number>> // number映射类型
ts
// ---- 映射类型:遍历对象的 key,生成新的类型 ----
// 大白话:就像"批量加工",把对象的每个属性都做同样的变换
// 生成 getter 类型
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P]
}
interface User {
id: number
name: string
email: string
}
type UserGetters = Getters<User>
// {
// getId: () => number
// getName: () => string
// getEmail: () => string
// }
// 实际应用:表单状态
type FormState<T> = {
[P in keyof T]: {
value: T[P]
error: string | null
touched: boolean
}
}
interface LoginForm {
username: string
password: string
}
type LoginFormState = FormState<LoginForm>
// {
// username: { value: string; error: string | null; touched: boolean }
// password: { value: string; error: string | null; touched: boolean }
// }实际应用示例
API 请求封装
ts
// ---- 泛型 API 请求函数 ----
// 大白话:就像"万能快递员",不管寄什么类型的包裹都能处理
interface ApiResponse<T> {
code: number
message: string
data: T
}
async function request<T>(
url: string,
options?: RequestInit
): Promise<ApiResponse<T>> {
const response = await fetch(url, options)
return response.json()
}
// 使用:指定 T 为 User,返回值自动有类型
interface User {
id: number
name: string
email: string
}
const response = await request<User>('/api/user/1')
console.log(response.data.name) // 类型安全,编辑器有提示
// 封装更高级的 API 客户端
class ApiClient {
constructor(private baseUrl: string) {}
async get<T>(endpoint: string): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`)
return response.json()
}
async post<T>(endpoint: string, data: any): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
return response.json()
}
}
const api = new ApiClient('https://api.example.com')
const users = await api.get<User[]>('/users') // User[] 类型
const newUser = await api.post<User>('/users', { name: '张三' }) // User 类型状态管理
ts
// ---- 泛型状态管理 ----
// 大白话:就像"万能仓库",不管存什么类型的货物都能管理
class Store<T extends Record<string, any>> {
// T extends Record<string, any>:T 必须是对象类型(不能是 number、string 等)
// Record<string, any> 表示"键是字符串,值是任意类型"的对象
private state: T
private listeners: Set<() => void> = new Set()
constructor(initialState: T) {
this.state = initialState
}
getState(): T {
return this.state
}
// 为什么用 Partial<T>?
// 因为更新状态时,通常只需要修改部分属性,不需要传整个对象
// 例如:store.setState({ loading: true }) 只更新 loading,不传 user
// Partial<T> 让 T 的所有属性变成可选,这样可以只传需要更新的字段
setState(partial: Partial<T>): void {
// 用展开运算符合并:保留旧状态,覆盖新状态
this.state = { ...this.state, ...partial }
// 通知所有订阅者:状态变了,该更新了
this.listeners.forEach(listener => listener())
}
subscribe(listener: () => void): () => void {
this.listeners.add(listener)
// 返回取消订阅函数:调用后不再监听状态变化
return () => this.listeners.delete(listener)
}
}
// 使用
interface AppState {
user: User | null
theme: 'light' | 'dark'
loading: boolean
}
const store = new Store<AppState>({
user: null,
theme: 'light',
loading: false,
})
// Partial<AppState> = { user?: User | null; theme?: 'light' | 'dark'; loading?: boolean }
// 所以可以只传部分属性
store.setState({ loading: true }) // ✅ 只更新 loading
store.setState({ user: { id: 1, name: '张三', email: 'xxx' } }) // ✅ 只更新 user
// store.setState({ xxx: 123 }) // ❌ xxx 不是 AppState 的属性表单验证
ts
// ---- 泛型表单验证器 ----
// 大白话:就像"万能检查员",不管检查什么类型的表单都能用
// 验证规则接口:每个规则有一个验证函数和错误提示
interface ValidationRule<T> {
validate: (value: T) => boolean // 验证函数:返回 true 表示通过
message: string // 验证失败时的错误提示
}
// T extends Record<string, any>:T 必须是对象类型(表单数据)
// Record<string, any> 表示键值对对象,确保 T 有字段可以验证
class FormValidator<T extends Record<string, any>> {
// Map 的 key 是表单字段名(如 'username'),value 是该字段的验证规则数组
// keyof T 保证 key 一定是 T 的属性名,不会拼错
private rules: Map<keyof T, ValidationRule<T[keyof T]>[]> = new Map()
// K extends keyof T:K 必须是 T 的某个属性名(如 'username')
// T[K]:该属性的类型(如 string)
// 返回 this:支持链式调用(validator.addRule(...).addRule(...))
addRule<K extends keyof T>(field: K, rule: ValidationRule<T[K]>): this {
const existing = this.rules.get(field) || []
this.rules.set(field, [...existing, rule] as any)
return this
}
// 返回值:valid 表示整体是否通过,errors 是每个字段的错误信息
// Partial<Record<keyof T, string>>:错误信息对象,每个字段可选
validate(data: T): { valid: boolean; errors: Partial<Record<keyof T, string>> } {
const errors: Partial<Record<keyof T, string>> = {}
let valid = true
// 遍历所有字段的验证规则
for (const [field, rules] of this.rules) {
for (const rule of rules) {
// data[field] 获取该字段的值,传给验证函数
if (!rule.validate(data[field])) {
errors[field] = rule.message // 记录错误信息
valid = false
break // 一个字段只要有一个规则失败就停止
}
}
}
return { valid, errors }
}
}
// 使用
interface FormData {
username: string
email: string
age: number
}
// 泛型参数 FormData 保证 addRule 只能用 'username' | 'email' | 'age' 作为字段名
const validator = new FormValidator<FormData>()
.addRule('username', {
validate: (value) => value.length >= 3, // value 自动推断为 string
message: '用户名至少 3 个字符',
})
.addRule('email', {
validate: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), // value 是 string
message: '邮箱格式不正确',
})
.addRule('age', {
validate: (value) => value >= 18, // value 自动推断为 number
message: '年龄必须大于等于 18',
})
const result = validator.validate({
username: 'ab',
email: 'invalid',
age: 16,
})
// { valid: false, errors: { username: '...', email: '...', age: '...' } }通用事件系统
ts
// ---- 泛型事件系统 ----
// 大白话:就像"万能事件中心",不管什么类型的事件都能处理
class EventEmitter<TEvents extends Record<string, any>> {
private listeners: Map<keyof TEvents, Set<Function>> = new Map()
on<K extends keyof TEvents>(event: K, handler: (data: TEvents[K]) => void): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set())
}
this.listeners.get(event)!.add(handler)
}
off<K extends keyof TEvents>(event: K, handler: (data: TEvents[K]) => void): void {
this.listeners.get(event)?.delete(handler)
}
emit<K extends keyof TEvents>(event: K, data: TEvents[K]): void {
this.listeners.get(event)?.forEach(handler => handler(data))
}
}
// 使用
interface AppEvents {
login: { userId: string; timestamp: Date }
logout: { userId: string }
error: { message: string; code: number }
}
const emitter = new EventEmitter<AppEvents>()
emitter.on('login', (data) => {
console.log(`用户 ${data.userId} 登录`) // data 有类型提示
})
emitter.emit('login', { userId: '123', timestamp: new Date() })
// emitter.emit('unknown', {}) // ❌ 事件名不存在常见问题
泛型类型推断失败
ts
// 问题:类型推断失败
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 }
}
// 解决:明确指定类型
const result = merge<User, { role: string }>(
{ id: 1, name: '张三', email: '[email protected]' },
{ role: 'admin' }
)泛型约束过宽
ts
// 问题:约束过宽,T 可能没有 length 属性
function getLength<T>(arg: T): number {
return arg.length // ❌ 类型 'T' 上不存在属性 'length'
}
// 解决:添加约束
function getLength<T extends { length: number }>(arg: T): number {
return arg.length // ✅
}泛型默认值与约束冲突
ts
// 问题:默认值不满足约束(TS 报错:Type '{ name: string }' does not satisfy the constraint)
interface Container<T extends { id: number } = { name: string }> {
data: T
}
// 解决:确保默认值满足约束(默认类型必须 extends 约束类型)
interface Container<T extends { id: number } = { id: number; name: string }> {
data: T
}参考
Vue/Vite 项目实际示例
泛型 Composable:useFetch
ts
// ---- 泛型 Composable:通用数据请求 ----
// 为什么用泛型?因为不同的 API 返回的数据类型不同
// 泛型让 useFetch 能适配任何数据类型,同时保留类型信息
import { ref, readonly } from 'vue'
// T 是返回数据的类型,默认 unknown
function useFetch<T = unknown>(url: string) {
// ref<T | null>:数据可能是 T 类型,也可能是 null(初始状态)
const data = ref<T | null>(null)
const loading = ref(false)
const error = ref<string | null>(null)
async function execute() {
loading.value = true
error.value = null
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
// as T:告诉 TS 响应体的类型是 T
data.value = await response.json() as T
} catch (e) {
// e 是 unknown 类型,需要类型断言
error.value = e instanceof Error ? e.message : '请求失败'
} finally {
loading.value = false
}
}
return {
data: readonly(data), // 只读,外部不能直接修改
loading: readonly(loading),
error: readonly(error),
execute, // 调用后才发请求
}
}
// 使用:指定 T 为 User[],data 自动变成 User[] | null
interface User {
id: number
name: string
email: string
}
const { data: users, loading, error, execute } = useFetch<User[]>('/api/users')
// users.value 的类型是 User[] | null,编辑器有完整提示
// users.value?.[0].name // ✅ 类型安全泛型组件类型工具
ts
// ---- 泛型组件类型工具 ----
// 场景:需要引用子组件实例,但不知道具体类型
import { ref } from 'vue'
// 获取组件实例类型
type ComponentInstance<T> = T extends new (...args: any[]) => infer I ? I : never
// 通用的组件引用 Hook
function useComponentRef<T extends abstract new (...args: any) => any>() {
return ref<InstanceType<T>>()
}
// 实际用法:表单组件引用
import MyForm from './MyForm.vue'
const formRef = ref<InstanceType<typeof MyForm>>()
// 调用子组件暴露的方法
async function handleSubmit() {
const valid = await formRef.value?.validate()
if (valid) {
// 提交逻辑
}
}