TS 泛型与断言
为什么要用泛型?
假设有一个简单的需求:封装一个函数,传入任意类型的参数,返回该参数本身。
对于 JavaScript 来说,这非常简单:
const fn = value => value
但在 TypeScript 中,我们可能会想到上一章学过的函数重载:
function fn(value: number): number
function fn(value: string): string
function fn(value: boolean): boolean
function fn(value: unknown): unknown
function fn(value: unknown): unknown {
return value
}
这种写法虽然可行,但需要为每种类型都写一遍重载。如果类型更多,代码会变得非常冗长。
泛型的基本用法
泛型允许在定义函数、接口或类时不指定具体类型,而是在使用时再指定。泛型利用了 TypeScript 的类型自动推导能力,可以让我们写出更灵活的代码:
function identity<T>(v: T): T {
return v
}
function fn<T>(value: T): T {
return value
}
const num = fn(42) // number
const str = fn('hello') // string
const bool = fn(true) // boolean
在函数名后使用 <> 来定义泛型 T,T 就像一个占位符,表示传递的类型。
在接口中使用泛型
在实际开发中,后端返回的数据通常具有 code、data、message 这样的结构。其中 code 和 message 是固定的,但 data 的内容是不确定的,这时就需要用到泛型:
interface ResultData<T> {
message: string
code: number
data: T
}
interface User {
id: number
name: string
age: number
}
type ResponseUserData = ResultData<User>
// {
// message: string
// code: number
// data: User
// }
💡 tips:泛型通常使用大写字母表示,常见命名有
T(Type)、K(Key)、V(Value)等。
多泛型
泛型可以定义多个,用逗号分隔:
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]]
}
const result = swap([1, 'hello'])
// [string, number]
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 }
}
const merged = merge({ name: 'Alice' }, { age: 18 })
// { name: string } & { age: number }
泛型的默认类型
泛型可以指定默认类型,类似于函数的默认参数:
interface ResultData<T = any> {
message: string
code: number
data: T
}
interface User {
id: number
name: string
age: number
}
// 使用默认类型
type ResponseData = ResultData // data 的类型是 any
// 指定具体类型
type ResponseUserData = ResultData<User> // data 的类型是 User
💡 tips:当泛型参数较多,且大部分场景使用相同类型时,设置默认类型可以简化代码。
泛型约束
泛型虽然灵活,但有时过于宽泛,需要添加约束来限定泛型的范围。
使用 extends 约束
function getLength<T extends { length: number }>(value: T): number {
return value.length
}
getLength('hello') // ✅ string 有 length 属性
getLength([1, 2, 3]) // ✅ array 有 length 属性
getLength({ length: 10 }) // ✅ 对象有 length 属性
getLength(42) // ❌ number 没有 length 属性
结合 keyof 的约束
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = {
name: 'Alice',
age: 18,
email: 'alice@example.com'
}
const name = getProperty(user, 'name') // string
const age = getProperty(user, 'age') // number
const email = getProperty(user, 'email') // string
getProperty(user, 'gender')
// ❌ Argument of type '"gender"' is not assignable to parameter of type '"name" | "age" | "email"'.
⚠️ 泛型约束能让代码更安全,但也要注意不要过度约束,影响泛型的灵活性。
类型断言 as
虽然 TypeScript 会进行类型推导,但有时我们需要告诉编译器「我知道这是什么类型」,这时可以使用类型断言:
type MyType = string | number | boolean
function getLength(value: MyType) {
console.log((value as string).length)
}
// 在类型非常确定的情况下使用
const inputDom = document.querySelector('input')
inputDom!.addEventListener('change', () => {
console.log((inputDom as HTMLInputElement).value)
})
⚠️ 类型断言有明显的类型安全隐患,使用时必须明确知道值的实际类型,否则可能导致运行时错误。
非空断言 !
当确定值不是 null 或 undefined 时,可以使用 ! 进行非空断言:
function getRandom(length?: number) {
if (!length) {
return undefined
}
return Math.random().toString(36).slice(-length)
}
const s = getRandom(6)
// 如果没有 !,TypeScript 会报错:'s' is possibly 'undefined'
s!.length
⚠️ 使用非空断言时需要谨慎,确保值确实不为
null或undefined。
satisfies 操作符
satisfies 是 TypeScript 4.9 引入的类型操作符,它与 as 断言类似,但更安全且智能。
为什么使用 satisfies?
与 as 断言相比,satisfies 有两个优势:
- 在满足类型安全的前提下进行验证
- 保留字面量类型,提供更精确的类型提示
案例对比
interface User {
name: string
age: number
[key: string]: any
}
// 使用类型注解:类型被拓宽,丢失额外属性的类型信息
const u1: User = {
name: '张三',
age: 18,
id: 123
}
u1.id // ❌ 无提示,id 的类型是 any
// 使用 satisfies:保留精确类型
const u2 = {
name: '张三',
age: 18,
id: 123
} satisfies User
u2.id // ✅ 有提示,id 的类型是 number
安全性验证
interface IConfig {
test: string | number
}
// 使用 as:不安全的断言
const t1 = {} as IConfig
t1.test // ❌ t 中实际没有 test,但不会报错
// 使用 satisfies:编译时验证
const t2 = {} satisfies IConfig
// ❌ Type '{}' does not satisfy the expected type 'IConfig'.
// Property 'test' is missing in type '{}' but required in type 'IConfig'.
const t3 = {
test: 123
} satisfies IConfig // ✅ 同时能根据 test 的值进行类型收窄
💡 tips:优先使用
satisfies替代as断言,既能保证类型安全,又能获得精确的类型提示。