类型守卫:精准确认每一处类型

目录

第 1 节 概述

前几篇我们学会了用类型标注和泛型来描述数据。但代码运行时,类型信息被擦除了——运行时没有类型,只有值

怎么确保在运行时安全地使用联合类型中的某一分支?怎么告诉 TypeScript “这个变量现在就应该是这个类型”?

类型守卫(Type Narrowing) 就是干这个的。它让 TypeScript 在特定代码分支中,将一个宽类型缩小为一个窄类型。

第 2 节 typeof 守卫

typeof 操作符可以在运行时检查值的基本类型,TypeScript 能理解这个检查:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function format(value: string | number) {
  if (typeof value === "string") {
    // 这里 value 的类型被缩小为 string
    return value.toUpperCase()
  }
  // 这里 value 的类型被缩小为 number
  return value.toFixed(2)
}

format("hello")  // "HELLO"
format(3.1415)   // "3.14"

typeof 能识别的类型有:stringnumberbigintbooleansymbolundefinedobjectfunction

1
2
3
4
5
6
7
function log(value: unknown) {
  if (typeof value === "function") {
    value()  // ✅ function 类型
  } else if (typeof value === "object" && value !== null) {
    console.log(value)  // ✅ object 类型(非 null)
  }
}

第 3 节 真值守卫

JavaScript 中,nullundefined0""falseNaN 都是假值。利用真假值检查可以缩小类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function getLength(value?: string | null) {
  // value 可能是 string | undefined | null
  if (value) {
    // 这里 value 被缩小为 string(假值被排除)
    return value.length
  }
  return 0
}

function printAll(values?: string[] | null) {
  if (values && values.length > 0) {
    // 先排除 null/undefined,再取值
    for (const v of values) {
      console.log(v)
    }
  }
}
注意

真值守卫会排除 0""。如果这些值是合法的,不要用真值守卫:

1
2
3
4
5
function process(score: number | null) {
  if (score) {  // ❌ 0 是合法的分数,但不能通过守卫
    // ...
  }
}

第 4 节 等值守卫

===!== 比较也能缩小类型范围:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function move(direction: "up" | "down" | "left" | "right" | null) {
  if (direction === null) {
    return  // direction 在这里是 null
  }

  if (direction === "up") {
    // direction 在这里是 "up"
  }
  // 剩下的分支:"down" | "left" | "right"
}

第 5 节 in 操作符

JavaScript 的 in 操作符检查对象是否有某个属性。TypeScript 能用它区分联合类型的成员:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
interface Fish {
  swim(): void
  finCount: number
}

interface Bird {
  fly(): void
  wingSpan: number
}

type Pet = Fish | Bird

function handlePet(pet: Pet) {
  if ("swim" in pet) {
    // 这里 pet 被缩小为 Fish
    console.log(`鱼有 ${pet.finCount} 个鳍`)
  } else {
    // 这里 pet 被缩小为 Bird
    console.log(`鸟的翼展是 ${pet.wingSpan} 米`)
  }
}

第 6 节 instanceof 守卫

instanceof 用于检查对象是否属于某个类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Dog {
  bark() { console.log("汪汪") }
}

class Cat {
  meow() { console.log("喵喵") }
}

type Pet = Dog | Cat

function makeSound(pet: Pet) {
  if (pet instanceof Dog) {
    pet.bark()   // ✅ Dog 类型
  } else {
    pet.meow()   // ✅ Cat 类型
  }
}

第 7 节 可辨识联合

可辨识联合是最强大的类型守卫模式。它的核心是:联合类型的每个成员都有一个相同的标识属性(通常是 typekind):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
type Circle = {
  kind: "circle"
  radius: number
}

type Rectangle = {
  kind: "rectangle"
  width: number
  height: number
}

type Triangle = {
  kind: "triangle"
  base: number
  height: number
}

type Shape = Circle | Rectangle | Triangle

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2
    case "rectangle":
      return shape.width * shape.height
    case "triangle":
      return (shape.base * shape.height) / 2
  }
}

每个 case 分支中,TypeScript 都知道对应的具体类型,可以安全地访问专属属性。

技巧

可辨识联合是 TypeScript 中最常用也最优雅的模式之一。后端 API 响应、状态管理、AST 节点等场景都会用到。用字符串字面量作为标识属性,可以同时享受自动补全和类型安全。

第 8 节 类型断言

当你知道某个值的类型比 TypeScript 推断的更精确时,可以用类型断言告诉编译器:“相信我,我知道我在做什么。”

1
2
3
4
5
6
7
// 获取 DOM 元素
const canvas = document.getElementById("canvas") as HTMLCanvasElement
// canvas 的类型被断言为 HTMLCanvasElement,而不是 HTMLElement | null

// 从宽类型断言为窄类型
const value: unknown = "hello"
const length = (value as string).length

两种断言语法等价:

1
2
const a = value as string   // JSX 中推荐
const b = <string>value     // JSX 中与标签冲突,不推荐

非空断言——告诉 TypeScript 这个值不是 nullundefined

1
2
3
function process(id?: string) {
  console.log(id!.toUpperCase())  // 告诉编译器 id 一定存在
}
危险

类型断言绕过了类型检查,使用要克制。先试试类型守卫,实在不行再用断言。特别是 as any——几乎永远不需要。

第 9 节 satisfies 操作符

satisfies 是 TypeScript 4.9 引入的运算符。它检查变量的类型是否符合某个类型,但保留变量的精确推断类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type Color = "red" | "green" | "blue"
type Palette = Record<string, Color>

const palette = {
  primary: "blue",
  secondary: "green",
  danger: "red",
} satisfies Palette

// palette.primary 的类型是 "blue"(字面量),而不是 Color
// 但 palette 整体满足 Palette 的约束

const wrongPalette = {
  primary: "blue",
  secondary: "yellow",  // ❌ "yellow" 不在 Color 中
} satisfies Palette

对比一下传统写法:

1
2
3
4
5
6
7
// 方式一:直接注解 —— 失去了精确的字面量类型
const p1: Palette = { primary: "blue", secondary: "green" }
p1.primary  // 类型为 Color("red" | "green" | "blue"),不是 "blue"

// 方式二:satisfies —— 既验证,又保留精确类型
const p2 = { primary: "blue", secondary: "green" } satisfies Palette
p2.primary  // 类型为 "blue"(精确字面量)

类型守卫和断言让你能在运行时和类型系统之间安全地穿行。

到现在为止,学的东西基本都在一个文件里。但真实项目代码是分文件的。 下一篇讲模块——怎么组织多文件 TypeScript 项目。

目录