59. 类型守卫与类型断言
📖 概述
类型守卫和类型断言是TypeScript中用于处理类型不确定性的重要工具。在Vue 3 + TypeScript项目中,它们能够帮助你在复杂的类型场景下编写更安全、更简洁的代码。本集将深入讲解类型守卫和类型断言的概念、分类、使用场景和最佳实践,帮助你在实际开发中灵活运用这些类型系统特性。
✨ 核心知识点
1. 类型守卫概述
什么是类型守卫
- 类型守卫是一种TypeScript语法,用于在运行时检查变量的类型
- 类型守卫能够缩小变量的类型范围,让TypeScript在编译时更准确地推断类型
- 类型守卫通常返回一个布尔值,表示变量是否属于某种类型
类型守卫的分类
- typeof类型守卫:用于检查基本类型
- instanceof类型守卫:用于检查对象实例
- in类型守卫:用于检查对象属性
- 自定义类型守卫:根据特定条件检查类型
- 字面量类型守卫:用于检查字面量类型
- 联合类型的类型守卫:用于检查联合类型成员
2. typeof类型守卫
基本用法
// typeof类型守卫
function isString(value: unknown): value is string {
return typeof value === 'string'
}
function isNumber(value: unknown): value is number {
return typeof value === 'number'
}
function isBoolean(value: unknown): value is boolean {
return typeof value === 'boolean'
}
function isFunction(value: unknown): value is Function {
return typeof value === 'function'
}
function isObject(value: unknown): value is object {
return typeof value === 'object' && value !== null
}在Vue组件中的应用
<script setup lang="ts">
import { ref } from 'vue'
const userInput = ref<unknown>('')
const message = ref('')
function handleInput(value: unknown) {
userInput.value = value
if (isString(value)) {
// TypeScript知道value是string类型
message.value = `您输入的是字符串:${value.toUpperCase()}`
} else if (isNumber(value)) {
// TypeScript知道value是number类型
message.value = `您输入的是数字:${value.toFixed(2)}`
} else if (isBoolean(value)) {
// TypeScript知道value是boolean类型
message.value = `您输入的是布尔值:${value ? 'true' : 'false'}`
} else {
message.value = '输入类型不支持'
}
}
// 测试
handleInput('hello') // 您输入的是字符串:HELLO
handleInput(123.456) // 您输入的是数字:123.46
handleInput(true) // 您输入的是布尔值:true
</script>
<template>
<div>
<h2>typeof类型守卫示例</h2>
<p>消息:{{ message }}</p>
</div>
</template>3. instanceof类型守卫
基本用法
// 定义类
class Animal {
name: string
constructor(name: string) {
this.name = name
}
}
class Dog extends Animal {
bark() {
return '汪汪'
}
}
class Cat extends Animal {
meow() {
return '喵喵'
}
}
// instanceof类型守卫
function isDog(animal: Animal): animal is Dog {
return animal instanceof Dog
}
function isCat(animal: Animal): animal is Cat {
return animal instanceof Cat
}在Vue组件中的应用
<script setup lang="ts">
import { ref } from 'vue'
// 定义类
class Animal {
name: string
constructor(name: string) {
this.name = name
}
}
class Dog extends Animal {
bark() {
return '汪汪'
}
}
class Cat extends Animal {
meow() {
return '喵喵'
}
}
// instanceof类型守卫
function isDog(animal: Animal): animal is Dog {
return animal instanceof Dog
}
function isCat(animal: Animal): animal is Cat {
return animal instanceof Cat
}
const animals = ref<Animal[]>([
new Dog('旺财'),
new Cat('咪咪'),
new Dog('小黑')
])
const messages = ref<string[]>([])
function makeSound(animal: Animal) {
if (isDog(animal)) {
// TypeScript知道animal是Dog类型
messages.value.push(`${animal.name}:${animal.bark()}`)
} else if (isCat(animal)) {
// TypeScript知道animal是Cat类型
messages.value.push(`${animal.name}:${animal.meow()}`)
}
}
// 测试
animals.value.forEach(makeSound)
// 结果:[
// '旺财:汪汪',
// '咪咪:喵喵',
// '小黑:汪汪'
// ]
</script>
<template>
<div>
<h2>instanceof类型守卫示例</h2>
<ul>
<li v-for="(msg, index) in messages" :key="index">
{{ msg }}
</li>
</ul>
</div>
</template>4. in类型守卫
基本用法
// 定义接口
interface User {
name: string
email: string
}
interface Admin {
name: string
role: string
permissions: string[]
}
type Person = User | Admin
// in类型守卫
function isAdmin(person: Person): person is Admin {
return 'role' in person
}
function isUser(person: Person): person is User {
return 'email' in person
}在Vue组件中的应用
<script setup lang="ts">
import { ref } from 'vue'
// 定义接口
interface User {
name: string
email: string
}
interface Admin {
name: string
role: string
permissions: string[]
}
type Person = User | Admin
// in类型守卫
function isAdmin(person: Person): person is Admin {
return 'role' in person
}
const people = ref<Person[]>([
{
name: '张三',
email: 'zhangsan@example.com'
},
{
name: '李四',
role: 'admin',
permissions: ['read', 'write', 'delete']
},
{
name: '王五',
email: 'wangwu@example.com'
}
])
const adminNames = ref<string[]>([])
const userNames = ref<string[]>([])
// 分类处理
people.value.forEach(person => {
if (isAdmin(person)) {
// TypeScript知道person是Admin类型
adminNames.value.push(`${person.name} (${person.role})`)
} else {
// TypeScript知道person是User类型
userNames.value.push(person.name)
}
})
</script>
<template>
<div>
<h2>in类型守卫示例</h2>
<div>
<h3>管理员</h3>
<ul>
<li v-for="(name, index) in adminNames" :key="index">
{{ name }}
</li>
</ul>
</div>
<div>
<h3>普通用户</h3>
<ul>
<li v-for="(name, index) in userNames" :key="index">
{{ name }}
</li>
</ul>
</div>
</div>
</template>5. 自定义类型守卫
基本用法
// 定义接口
interface Circle {
kind: 'circle'
radius: number
}
interface Square {
kind: 'square'
sideLength: number
}
interface Rectangle {
kind: 'rectangle'
width: number
height: number
}
type Shape = Circle | Square | Rectangle
// 自定义类型守卫
function isCircle(shape: Shape): shape is Circle {
return shape.kind === 'circle'
}
function isSquare(shape: Shape): shape is Square {
return shape.kind === 'square'
}
function isRectangle(shape: Shape): shape is Rectangle {
return shape.kind === 'rectangle'
}
// 使用类型守卫计算面积
function getArea(shape: Shape): number {
if (isCircle(shape)) {
return Math.PI * shape.radius ** 2
} else if (isSquare(shape)) {
return shape.sideLength ** 2
} else {
// TypeScript知道这是Rectangle类型
return shape.width * shape.height
}
}在Vue组件中的应用
<script setup lang="ts">
import { ref } from 'vue'
// 定义接口
interface Circle {
kind: 'circle'
radius: number
}
interface Square {
kind: 'square'
sideLength: number
}
interface Rectangle {
kind: 'rectangle'
width: number
height: number
}
type Shape = Circle | Square | Rectangle
// 自定义类型守卫
function isCircle(shape: Shape): shape is Circle {
return shape.kind === 'circle'
}
function isSquare(shape: Shape): shape is Square {
return shape.kind === 'square'
}
function isRectangle(shape: Shape): shape is Rectangle {
return shape.kind === 'rectangle'
}
// 使用类型守卫计算面积
function getArea(shape: Shape): number {
if (isCircle(shape)) {
return Math.PI * shape.radius ** 2
} else if (isSquare(shape)) {
return shape.sideLength ** 2
} else {
// TypeScript知道这是Rectangle类型
return shape.width * shape.height
}
}
const shapes = ref<Shape[]>([
{ kind: 'circle', radius: 5 },
{ kind: 'square', sideLength: 4 },
{ kind: 'rectangle', width: 3, height: 6 }
])
const areas = ref<{ name: string; area: number }[]>([])
// 计算每个形状的面积
shapes.value.forEach(shape => {
const area = getArea(shape)
let name = ''
if (isCircle(shape)) {
name = `圆形 (半径: ${shape.radius})`
} else if (isSquare(shape)) {
name = `正方形 (边长: ${shape.sideLength})`
} else {
name = `矩形 (${shape.width} × ${shape.height})`
}
areas.value.push({ name, area: parseFloat(area.toFixed(2)) })
})
</script>
<template>
<div>
<h2>自定义类型守卫示例</h2>
<ul>
<li v-for="(item, index) in areas" :key="index">
{{ item.name }}:面积 = {{ item.area }}
</li>
</ul>
</div>
</template>6. 类型断言
什么是类型断言
- 类型断言是一种TypeScript语法,用于告诉编译器你比它更了解变量的类型
- 类型断言不会在运行时执行,只是在编译时起作用
- 类型断言可以通过两种语法实现:
as语法和尖括号语法
基本用法
// as语法
const someValue: unknown = 'this is a string'
const strLength1: number = (someValue as string).length
// 尖括号语法(在JSX中不支持)
const strLength2: number = (<string>someValue).length在Vue组件中的应用
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const canvasRef = ref<HTMLCanvasElement | null>(null)
onMounted(() => {
// 使用类型断言获取canvas元素
const canvas = canvasRef.value as HTMLCanvasElement
// 现在TypeScript知道canvas是HTMLCanvasElement类型
const ctx = canvas.getContext('2d')
if (ctx) {
ctx.fillStyle = 'red'
ctx.fillRect(10, 10, 100, 100)
}
})
</script>
<template>
<div>
<h2>类型断言示例</h2>
<canvas ref="canvasRef" width="200" height="200" style="border: 1px solid black;"></canvas>
</div>
</template>7. 非空断言
基本用法
// 非空断言使用!符号
let element: HTMLElement | null = document.getElementById('app')
// 使用!断言element不为null
const width: number = element!.offsetWidth在Vue组件中的应用
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const inputRef = ref<HTMLInputElement | null>(null)
onMounted(() => {
// 使用非空断言,假设inputRef.value一定存在
inputRef.value!.focus()
})
</script>
<template>
<div>
<h2>非空断言示例</h2>
<input ref="inputRef" type="text" placeholder="请输入内容">
</div>
</template>8. 类型守卫与类型断言的区别
| 特性 | 类型守卫 | 类型断言 |
|---|---|---|
| 目的 | 缩小类型范围,让TypeScript更准确地推断类型 | 告诉编译器你比它更了解变量的类型 |
| 运行时 | 会在运行时执行检查 | 仅在编译时起作用,运行时无影响 |
| 安全性 | 更安全,因为有运行时检查 | 安全性较低,依赖开发者的判断 |
| 语法 | 函数返回类型为value is Type |
as Type或<Type>value |
| 适用场景 | 复杂的类型检查,联合类型处理 | 明确知道变量类型,修复编译错误 |
📝 最佳实践
优先使用类型守卫,而不是类型断言
- 类型守卫更安全,有运行时检查
- 类型断言依赖开发者的判断,容易出错
- 类型守卫能够提供更好的类型推断
合理使用类型断言
- 只在明确知道变量类型时使用
- 避免过度使用类型断言,尤其是
any类型 - 使用类型断言时添加注释,说明原因
使用非空断言要谨慎
- 非空断言会忽略TypeScript的空值检查
- 确保变量确实不会为空,否则会导致运行时错误
- 优先使用可选链操作符
?.和空值合并操作符??
选择合适的类型守卫
- 基本类型使用
typeof类型守卫 - 对象实例使用
instanceof类型守卫 - 接口联合类型使用
in类型守卫 - 复杂条件使用自定义类型守卫
- 基本类型使用
为类型守卫添加文档注释
- 说明类型守卫的用途和条件
- 提高代码的可读性和可维护性
结合类型守卫和泛型
- 创建通用的类型守卫函数
- 提高代码的复用性
避免类型守卫的嵌套过深
- 保持类型守卫的简洁性
- 考虑重构复杂的类型检查逻辑
使用类型守卫处理异步数据
- 在获取异步数据后,使用类型守卫检查数据结构
- 确保数据类型符合预期,避免运行时错误
💡 常见问题与解决方案
类型守卫函数返回值类型错误
- 确保类型守卫函数的返回类型是
value is Type形式 - 检查返回值的逻辑是否正确
- 确保类型守卫函数的返回类型是
类型断言导致运行时错误
- 避免对不确定类型的变量使用类型断言
- 优先使用类型守卫进行检查
- 在使用类型断言前,添加运行时检查
非空断言导致空指针异常
- 检查变量是否确实不会为空
- 优先使用可选链操作符
?. - 添加空值检查
类型守卫的性能问题
- 避免在频繁调用的函数中使用复杂的类型守卫
- 考虑缓存类型检查结果
- 优化类型守卫的逻辑
类型守卫无法处理所有情况
- 确保类型守卫覆盖所有可能的类型
- 考虑使用
never类型检查是否有遗漏
类型断言与类型守卫的冲突
- 避免在同一个变量上同时使用类型断言和类型守卫
- 选择更适合的方式处理类型
📚 进一步学习资源
🎯 课后练习
基础练习
- 创建不同类型的类型守卫:typeof、instanceof、in
- 使用类型断言处理DOM元素
- 使用非空断言简化代码
进阶练习
- 创建一个复杂的自定义类型守卫
- 使用类型守卫处理异步数据
- 结合泛型和类型守卫
实战练习
- 在Vue组件中使用类型守卫处理联合类型
- 优化现有代码中的类型断言,使用类型守卫替代
- 创建一个通用的类型守卫工具函数
类型系统练习
- 分析TypeScript内置的类型守卫
- 学习第三方库中的类型守卫实现
- 尝试实现一个类型守卫库
通过本集的学习,你已经掌握了类型守卫和类型断言的相关知识,包括不同类型的类型守卫、类型断言的使用、非空断言、类型守卫与类型断言的区别等。在实际项目中,合理运用类型守卫和类型断言,能够确保代码的类型安全,提高开发效率和代码质量。下一集我们将深入学习严格模式配置与最佳实践,进一步提升Vue 3 + TypeScript的开发能力。