59. 类型守卫与类型断言

📖 概述

类型守卫和类型断言是TypeScript中用于处理类型不确定性的重要工具。在Vue 3 + TypeScript项目中,它们能够帮助你在复杂的类型场景下编写更安全、更简洁的代码。本集将深入讲解类型守卫和类型断言的概念、分类、使用场景和最佳实践,帮助你在实际开发中灵活运用这些类型系统特性。

✨ 核心知识点

1. 类型守卫概述

什么是类型守卫

  • 类型守卫是一种TypeScript语法,用于在运行时检查变量的类型
  • 类型守卫能够缩小变量的类型范围,让TypeScript在编译时更准确地推断类型
  • 类型守卫通常返回一个布尔值,表示变量是否属于某种类型

类型守卫的分类

  1. typeof类型守卫:用于检查基本类型
  2. instanceof类型守卫:用于检查对象实例
  3. in类型守卫:用于检查对象属性
  4. 自定义类型守卫:根据特定条件检查类型
  5. 字面量类型守卫:用于检查字面量类型
  6. 联合类型的类型守卫:用于检查联合类型成员

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&lt;Type&gt;value
适用场景 复杂的类型检查,联合类型处理 明确知道变量类型,修复编译错误

📝 最佳实践

  1. 优先使用类型守卫,而不是类型断言

    • 类型守卫更安全,有运行时检查
    • 类型断言依赖开发者的判断,容易出错
    • 类型守卫能够提供更好的类型推断
  2. 合理使用类型断言

    • 只在明确知道变量类型时使用
    • 避免过度使用类型断言,尤其是any类型
    • 使用类型断言时添加注释,说明原因
  3. 使用非空断言要谨慎

    • 非空断言会忽略TypeScript的空值检查
    • 确保变量确实不会为空,否则会导致运行时错误
    • 优先使用可选链操作符?.和空值合并操作符??
  4. 选择合适的类型守卫

    • 基本类型使用typeof类型守卫
    • 对象实例使用instanceof类型守卫
    • 接口联合类型使用in类型守卫
    • 复杂条件使用自定义类型守卫
  5. 为类型守卫添加文档注释

    • 说明类型守卫的用途和条件
    • 提高代码的可读性和可维护性
  6. 结合类型守卫和泛型

    • 创建通用的类型守卫函数
    • 提高代码的复用性
  7. 避免类型守卫的嵌套过深

    • 保持类型守卫的简洁性
    • 考虑重构复杂的类型检查逻辑
  8. 使用类型守卫处理异步数据

    • 在获取异步数据后,使用类型守卫检查数据结构
    • 确保数据类型符合预期,避免运行时错误

💡 常见问题与解决方案

  1. 类型守卫函数返回值类型错误

    • 确保类型守卫函数的返回类型是value is Type形式
    • 检查返回值的逻辑是否正确
  2. 类型断言导致运行时错误

    • 避免对不确定类型的变量使用类型断言
    • 优先使用类型守卫进行检查
    • 在使用类型断言前,添加运行时检查
  3. 非空断言导致空指针异常

    • 检查变量是否确实不会为空
    • 优先使用可选链操作符?.
    • 添加空值检查
  4. 类型守卫的性能问题

    • 避免在频繁调用的函数中使用复杂的类型守卫
    • 考虑缓存类型检查结果
    • 优化类型守卫的逻辑
  5. 类型守卫无法处理所有情况

    • 确保类型守卫覆盖所有可能的类型
    • 考虑使用never类型检查是否有遗漏
  6. 类型断言与类型守卫的冲突

    • 避免在同一个变量上同时使用类型断言和类型守卫
    • 选择更适合的方式处理类型

📚 进一步学习资源

🎯 课后练习

  1. 基础练习

    • 创建不同类型的类型守卫:typeof、instanceof、in
    • 使用类型断言处理DOM元素
    • 使用非空断言简化代码
  2. 进阶练习

    • 创建一个复杂的自定义类型守卫
    • 使用类型守卫处理异步数据
    • 结合泛型和类型守卫
  3. 实战练习

    • 在Vue组件中使用类型守卫处理联合类型
    • 优化现有代码中的类型断言,使用类型守卫替代
    • 创建一个通用的类型守卫工具函数
  4. 类型系统练习

    • 分析TypeScript内置的类型守卫
    • 学习第三方库中的类型守卫实现
    • 尝试实现一个类型守卫库

通过本集的学习,你已经掌握了类型守卫和类型断言的相关知识,包括不同类型的类型守卫、类型断言的使用、非空断言、类型守卫与类型断言的区别等。在实际项目中,合理运用类型守卫和类型断言,能够确保代码的类型安全,提高开发效率和代码质量。下一集我们将深入学习严格模式配置与最佳实践,进一步提升Vue 3 + TypeScript的开发能力。

« 上一篇 Vue3 + TypeScript 系列教程 - 第58集:第三方库类型声明 下一篇 » Vue3 + TypeScript 系列教程 - 第60集:严格模式配置与最佳实践