# 类型守卫：精准确认每一处类型

## 概述

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

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

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

## typeof 守卫

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

```typescript
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` 能识别的类型有：`string`、`number`、`bigint`、`boolean`、`symbol`、`undefined`、`object`、`function`。

```typescript
function log(value: unknown) {
  if (typeof value === "function") {
    value()  // ✅ function 类型
  } else if (typeof value === "object" && value !== null) {
    console.log(value)  // ✅ object 类型（非 null）
  }
}
```

## 真值守卫

JavaScript 中，`null`、`undefined`、`0`、`""`、`false`、`NaN` 都是假值。利用真假值检查可以缩小类型：

```typescript
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)
    }
  }
}
```

> [!warning] 注意
> 真值守卫会排除 `0` 和 `""`。如果这些值是合法的，不要用真值守卫：
> ```typescript
> function process(score: number | null) {
>   if (score) {  // ❌ 0 是合法的分数，但不能通过守卫
>     // ...
>   }
> }
> ```

## 等值守卫

用 `===`、`!==` 比较也能缩小类型范围：

```typescript
function move(direction: "up" | "down" | "left" | "right" | null) {
  if (direction === null) {
    return  // direction 在这里是 null
  }

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

## in 操作符

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

```typescript
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} 米`)
  }
}
```

## instanceof 守卫

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

```typescript
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 类型
  }
}
```

## 可辨识联合

可辨识联合是最强大的类型守卫模式。它的核心是：联合类型的每个成员都有一个相同的**标识属性**（通常是 `type` 或 `kind`）：

```typescript
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 都知道对应的具体类型，可以安全地访问专属属性。

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

## 类型断言

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

```typescript
// 获取 DOM 元素
const canvas = document.getElementById("canvas") as HTMLCanvasElement
// canvas 的类型被断言为 HTMLCanvasElement，而不是 HTMLElement | null

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

两种断言语法等价：

```typescript
const a = value as string   // JSX 中推荐
const b = <string>value     // JSX 中与标签冲突，不推荐
```

非空断言——告诉 TypeScript 这个值不是 `null` 或 `undefined`：

```typescript
function process(id?: string) {
  console.log(id!.toUpperCase())  // 告诉编译器 id 一定存在
}
```

> [!danger] 危险
> 类型断言绕过了类型检查，使用要克制。先试试类型守卫，实在不行再用断言。特别是 `as any`——几乎永远不需要。

## satisfies 操作符

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

```typescript
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
```

对比一下传统写法：

```typescript
// 方式一：直接注解 —— 失去了精确的字面量类型
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 项目。


---

> 作者: Aphros  
> URL: https://blog.papergate.top/posts/09.%E7%B1%BB%E5%9E%8B%E5%AE%88%E5%8D%AB%E7%B2%BE%E5%87%86%E7%A1%AE%E8%AE%A4%E6%AF%8F%E4%B8%80%E5%A4%84%E7%B1%BB%E5%9E%8B/  

