阅读本文你能学到什么?
- 类型断言是什么,as 怎么用
- 泛型是什么,为什么需要它
- 泛型接口、泛型别名、泛型函数的基本用法
- 泛型约束的作用与用法
前言
第四篇我们搞定了字面量类型和类型推论。但实际项目中,还有两个绕不开的问题:你比 TS 更清楚某个值的类型,怎么告诉它"相信我"?又或者,写一个函数,它能接收任意类型,又能保留传入的类型信息------怎么让类型跟着参数走?
第一个问题的答案是类型断言 ,第二个是泛型。
这一篇,咱们就把这两个技能一起搞定。
一、类型断言
1.1 什么是类型断言?
类型断言是告诉 TypeScript 编译器"我知道这个值是什么类型,你按我说的来"。它不会在运行时改变值,只是让 TS 通过编译。
下面通过两个常见场景来理解它的用法。
示例一:DOM 元素获取
比如 document.getElementById('username'),TypeScript 只知道它返回的是 HTMLElement | null------它只知道页面上有个元素叫 username,但不知道它具体是什么标签,可能是 input,也可能是 div。
但作为开发者,你清楚这个 username 对应的就是一个输入框:
typescript
const usernameInput = document.getElementById('username') as HTMLInputElement
// 现在 TS 知道它是 HTMLInputElement,可以放心访问 .value
console.log(usernameInput.value) // ✅
示例二:处理 unknown 类型
unknown 类型的值不能直接使用,需要用断言告诉 TS 它到底是什么类型。
typescript
let value: unknown = 'Hello'
// value.length // ❌ 报错:value 是 unknown
let len = (value as string).length // ✅ 断言为 string 后可以访问
1.2 语法:推荐使用 as
TypeScript 有两种断言语法:<类型>值 和 值 as 类型。在 .tsx 文件中,尖括号语法会与 JSX 标签产生解析冲突,所以推荐统一使用 as。
typescript
// ❌ 不推荐:尖括号语法(在 JSX 中会冲突)
let value = <string>'hello'
// ✅ 推荐:as 语法
let value = 'hello' as string
1.3 as const 断言
除了用 as 指定类型,TypeScript 还提供了 as const,用于控制类型推断的精度------它告诉 TypeScript 把类型推断为最精确的字面量类型。
typescript
// 没有 as const
let color = 'red' // 推断为 string
// 有 as const
let color = 'red' as const // 推断为 'red'
// 对象的 as const
const config = {
url: '/api',
method: 'GET'
} as const
// 推断为:{ readonly url: '/api'; readonly method: 'GET' }
as const 在需要精确字面量类型的场景非常有用,比如定义路由配置、常量对象等。
1.4 类型断言的注意事项
类型断言只能够"欺骗"TypeScript 编译器,无法避免运行时的错误。滥用类型断言可能会导致运行时错误。
typescript
function processValue(value: string | number) {
// 假设我们断言 value 是 number
const num = value as number
console.log(num.toFixed(2))
}
processValue('hello') // ❌ 编译通过,但运行时报错:num.toFixed is not a function
💡 给新手的提醒: 类型断言应该在你比 TS 更清楚类型 的时候使用,而不是用来绕过类型检查。如果你不确定值的类型,建议先用
typeof或instanceof在代码里做判断,而不是直接用断言。
二、泛型
2.1 什么是泛型?为什么需要它?
泛型(Generics)是指在定义接口、函数等类型的时候,不预先指定具体的类型,而在使用的时候再指定类型 的一种特性。使用泛型可以复用类型并且让类型更加灵活。
思考一下:下面的两种数据结构,如何使用 interface 接口实现类型注解?这样做有什么问题?
typescript
// 用户数据
interface UserData {
code: number
message: string
data: { id: number; name: string }
}
// 商品数据
interface ProductData {
code: number
message: string
data: { id: number; name: string; price: number }
}
我们可以对比下数据结构,这两个接口的结构完全一样,只有 data 的类型不同。如果每种数据类型都写一个接口,代码会变得非常冗余------泛型就是来解决这个问题的。
使用泛型后,只需要定义一次:
typescript
interface ResponseData<T> {
code: number
message: string
data: T
}
2.2 泛型接口
在接口interface后面使用 <T> 声明泛型参数,接口里的其他成员都可以使用该参数的类型。
语法: interface 接口名<T> { ... }
typescript
// 定义泛型接口
interface ResponseData<T> {
code: number
message: string
data: T
}
// 使用泛型接口
interface User {
id: number
name: string
}
interface Product {
id: number
name: string
price: number
}
const userResponse: ResponseData<User> = {
code: 200,
message: '获取用户成功',
data: { id: 1, name: '张三' }
}
const productResponse: ResponseData<Product> = {
code: 200,
message: '获取商品成功',
data: { id: 1001, name: '手机', price: 2999 }
}
我们可以把泛型理解成一个函数------
T就像类型的"参数",可以传一个,也可以传多个,比如ResponseData<T>、ResponseData<T, U>。
2.3 泛型别名
在类型别名 type 的后面使用 <T> 声明泛型参数,类型别名里的其他成员都可以使用该参数的类型。
语法: type 类型别名<T> = { ... }
typescript
// 定义泛型别名
type ResponseData<T> = {
code: number
message: string
data: T
}
type User = {
id: number
name: string
}
const response: ResponseData<User> = {
code: 200,
message: '获取用户成功',
data: { id: 1, name: '张三' }
}
2.4 泛型函数
在函数名称的后面使用 <T> 声明泛型参数,整个函数中(参数、返回值、函数体)都可以使用该参数的类型。
语法: function 函数名<T>(参数: T): T { ... }
typescript
// 需求:创建一个 createArray 函数
// 它可以创建一个指定长度的数组,同时将每一项都填充一个默认值(多种类型)
function createArray<T>(length: number, defaultValue: T): T[] {
const arr = new Array(length)
for (let i = 0; i < length; i++) {
arr[i] = defaultValue
}
return arr
}
// 使用示例
const strArray = createArray<string>(3, 'hello') // ['hello', 'hello', 'hello']
const numArray = createArray<number>(3, 100) // [100, 100, 100]
const boolArray = createArray<boolean>(3, true) // [true, true, true]
// 也可以让 TS 自动推断类型
const inferredArray = createArray(3, 'hello') // TS 自动推断为 string[]
2.5 泛型约束
泛型的特点就是灵活不确定,有些时候泛型函数的内部需要访问一些特定类型的数据才有的属性,此时会有类型错误。
typescript
function getLength<T>(value: T): number {
// ❌ 报错:T 类型上不一定有 length 属性
return value.length
}
解决方案:通过泛型约束来解决------使用 extends 关键字让传入的类型必须满足某个条件。
语法: T extends 类型 ------ 限制 T 必须是某个类型的子类型。
typescript
// 定义一个有 length 属性的接口
interface Length {
length: number
}
// 约束:T 必须具有 length 属性
function getLength<T extends Length>(value: T): number {
return value.length // ✅ 现在安全了
}
getLength('hello') // ✅ 字符串有 length
getLength([1, 2, 3]) // ✅ 数组有 length
getLength('abc') // ✅
// getLength(123) // ❌ 报错:数字没有 length 属性
三、开发中的常见错误与避坑指南
| 常见错误 | 原因 | 解决方法 |
|---|---|---|
| 滥用类型断言掩盖真实类型错误 | 断言只在编译时生效,运行时无法保证类型正确 | 先用 typeof 或 instanceof 做判断,断言只在你 100% 确定时使用 |
| 泛型函数内部访问属性报错 | 泛型太灵活,TS 无法确认类型是否有该属性 | 使用泛型约束 extends 限制类型 |
四、本篇小结
| 知识点 | 示例 | 说明 |
|---|---|---|
| 类型断言 as | value as string |
告诉 TS 值的类型 |
| as const | 'red' as const |
推断为最精确的字面量类型 |
| 泛型接口 | interface Res<T> { data: T } |
接口中使用泛型 |
| 泛型别名 | type Res<T> = { data: T } |
类型别名中使用泛型 |
| 泛型函数 | function fn<T>(v: T): T |
函数中使用泛型 |
| 泛型约束 | T extends Length |
限制 T 必须满足某个条件 |
核心记忆口诀
断言用 as,泛型占位 T 少不了。约束用 extends 定边界,复用灵活又可靠。
下篇预告
📖 上篇回顾:TypeScript 核心语法进阶 ------ 字面量类型与类型推论
下一篇将进入 TypeScript 实战,看看在实际的 Vue 3 项目中,如何将 TS 语法应用到组件和逻辑中。
欢迎持续关注 🚀