泛型很强大,但有时我们需要限制类型参数的范围。泛型约束允许我们指定类型参数必须满足某些条件。
基本约束#
使用 extends 关键字约束类型参数:
// TypeScript 5.x
// 没有约束:无法访问任何属性function logLength<T>(arg: T): T { // console.log(arg.length); // ❌ 错误:T 上不存在 length return arg}
// 添加约束:必须有 length 属性interface Lengthwise { length: number}
function logLength2<T extends Lengthwise>(arg: T): T { console.log(arg.length) // ✅ 现在可以访问 return arg}
logLength2('hello') // ✅ string 有 lengthlogLength2([1, 2, 3]) // ✅ 数组有 lengthlogLength2({ length: 10 }) // ✅ 对象有 length// logLength2(123); // ❌ number 没有 lengthkeyof 约束#
使用 keyof 约束类型参数为对象的键:
// 获取对象属性function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]}
const person = { name: '张三', age: 25, email: 'test@test.com',}
const name = getProperty(person, 'name') // stringconst age = getProperty(person, 'age') // number// getProperty(person, 'invalid'); // ❌ 错误
// 设置对象属性function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): void { obj[key] = value}
setProperty(person, 'age', 30) // ✅// setProperty(person, 'age', 'thirty'); // ❌ 类型不匹配多重约束#
一个类型参数可以有多个约束:
interface Named { name: string}
interface Aged { age: number}
// 使用交叉类型实现多重约束function greet<T extends Named & Aged>(entity: T): string { return `Hello, ${entity.name}! You are ${entity.age} years old.`}
greet({ name: '张三', age: 25 }) // ✅greet({ name: '张三', age: 25, email: 'test@test.com' }) // ✅ 可以有额外属性// greet({ name: '张三' }); // ❌ 缺少 age类类型约束#
约束类型参数为可构造的类型:
// 构造函数约束function create<T>(ctor: new () => T): T { return new ctor()}
class User { name = 'default'}
const user = create(User) // User
// 带参数的构造函数function createWithArgs<T>(ctor: new (...args: any[]) => T, ...args: any[]): T { return new ctor(...args)}
class Product { constructor( public name: string, public price: number ) {}}
const product = createWithArgs(Product, '手机', 5999)
// 更精确的类型约束type Constructor<T = {}> = new (...args: any[]) => T
function factory<T extends Constructor>(Base: T) { return class extends Base { timestamp = new Date() }}
const TimestampedUser = factory(User)const user2 = new TimestampedUser()console.log(user2.timestamp)条件类型与约束#
约束可以与条件类型结合:
// 提取数组元素类型type ElementType<T> = T extends (infer E)[] ? E : never
type StringElement = ElementType<string[]> // stringtype NumberElement = ElementType<number[]> // numbertype Never = ElementType<string> // never
// 提取函数返回类型type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never
type FnReturn = MyReturnType<() => string> // string
// 提取 Promise 内部类型type UnwrapPromise<T> = T extends Promise<infer U> ? U : T
type Unwrapped = UnwrapPromise<Promise<string>> // stringtype NotPromise = UnwrapPromise<number> // number约束传递#
类型参数之间可以相互约束:
// U 约束为 T 的子类型function copy<T extends U, U>(source: T, target: U): T { return { ...target, ...source }}
// K 约束为 T 的键function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> { const result = {} as Pick<T, K> keys.forEach((key) => { result[key] = obj[key] }) return result}
const user = { name: '张三', age: 25, email: 'test@test.com' }const picked = pick(user, ['name', 'age'])// { name: string; age: number }默认约束#
// 默认约束为 unknownfunction identity<T = unknown>(arg: T): T { return arg}
// 默认约束为特定类型interface Container<T extends object = Record<string, unknown>> { data: T}
const c1: Container = { data: { anything: 'here' } }const c2: Container<{ name: string }> = { data: { name: '张三' } }实际应用#
类型安全的事件系统#
interface EventMap { click: { x: number; y: number } input: { value: string } submit: { data: Record<string, unknown> }}
class TypedEventEmitter<T extends Record<string, unknown>> { private handlers = new Map<keyof T, Function[]>()
on<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void { const list = this.handlers.get(event) || [] list.push(handler) this.handlers.set(event, list) }
emit<K extends keyof T>(event: K, payload: T[K]): void { const handlers = this.handlers.get(event) || [] handlers.forEach((h) => h(payload)) }}
const emitter = new TypedEventEmitter<EventMap>()
emitter.on('click', (e) => { console.log(e.x, e.y) // 类型安全})
emitter.emit('click', { x: 100, y: 200 }) // ✅// emitter.emit('click', { value: 'test' }); // ❌ 类型不匹配类型安全的 API 客户端#
interface ApiEndpoints { '/users': { request: { page: number }; response: User[] } '/users/:id': { request: { id: string }; response: User } '/posts': { request: {}; response: Post[] }}
class ApiClient< T extends Record<string, { request: object; response: unknown }>,> { async get<K extends keyof T>( endpoint: K, params: T[K]['request'] ): Promise<T[K]['response']> { // 实现 API 调用 const response = await fetch(endpoint as string) return response.json() }}
const api = new ApiClient<ApiEndpoints>()
// 类型安全的 API 调用const users = await api.get('/users', { page: 1 }) // User[]const user = await api.get('/users/:id', { id: '123' }) // User深度只读#
type DeepReadonly<T> = T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T
interface User { name: string address: { city: string street: string }}
type ReadonlyUser = DeepReadonly<User>
const user: ReadonlyUser = { name: '张三', address: { city: '北京', street: '中关村', },}
// user.name = '李四'; // ❌ 只读// user.address.city = '上海'; // ❌ 深度只读常见问题#
🙋 约束和默认类型有什么关系?#
默认类型必须满足约束:
// ✅ 正确:string 满足 Lengthwiseinterface Lengthwise { length: number}function fn<T extends Lengthwise = string>(arg: T): T { return arg}
// ❌ 错误:number 不满足 Lengthwise// function fn2<T extends Lengthwise = number>(arg: T): T🙋 如何约束类型参数为原始类型?#
type Primitive = string | number | boolean | null | undefined | symbol | bigint
function process<T extends Primitive>(value: T): T { return value}
process('hello') // ✅process(42) // ✅// process({}); // ❌🙋 约束中的 extends 和继承中的 extends 一样吗?#
语法相同,但语义不同:
// 继承:创建子类class Dog extends Animal {}
// 约束:类型必须满足条件function fn<T extends Animal>(arg: T) {}总结#
| 约束类型 | 语法 | 用途 |
|---|---|---|
| 接口约束 | T extends Interface | 必须满足接口结构 |
| keyof 约束 | K extends keyof T | 必须是对象的键 |
| 多重约束 | T extends A & B | 必须满足多个条件 |
| 构造函数约束 | T extends new () => U | 必须是可构造类型 |
| 条件约束 | T extends ... ? ... : ... | 条件类型推断 |
下一篇我们将学习 TypeScript 内置的工具类型,如 Partial、Required、Pick 等。