首页 / TypeScript 入门教程 / 类型守卫与类型收窄(上)

TypeScript 入门教程

类型守卫与类型收窄(上)

本教程共 80 篇 · 第 51 篇 · 更新于 2026-08-10 · 约 12 分钟阅读

TypeScriptTypeScript 入门教程类型守卫类型收窄Narrowingtypeofinstanceof

本节目标:掌握 TypeScript 的自动类型收窄机制——typeof、truthiness、equality、instanceof、in 五种内置收窄方式,理解编译器如何通过控制流分析自动推断更精确的类型。

什么是类型收窄

一个变量声明时类型可能是 string | number,但在某个 if 分支里,你通过代码逻辑可以确定它一定是 string。TypeScript 的编译器能”看懂”这种判断,并自动把变量的类型从宽泛的联合类型收窄为具体类型,这个过程叫类型收窄(Narrowing)。

function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") {
    // 编译器知道:这里 padding 一定是 number
    return " ".repeat(padding) + input;
  }
  // 编译器知道:这里 padding 一定是 string
  return padding + input;
}

上例中,typeof padding === "number" 就是一种类型守卫。TypeScript 根据它把 if 里的 padding 收窄为 numberelse 里自动收窄为 string

typeof 收窄

typeof 是最常用的类型守卫,TypeScript 能识别以下几种返回值:

  • "string""number""bigint""boolean""symbol"
  • "undefined""object""function"
function processValue(value: string | number | boolean) {
  if (typeof value === "string") {
    console.log(value.toUpperCase()); // value 是 string
  } else if (typeof value === "number") {
    console.log(value.toFixed(2));     // value 是 number
  } else {
    console.log(value);               // value 是 boolean
  }
}

typeof 的坑:null 也是 “object”

function printAll(strs: string | string[] | null) {
  if (typeof strs === "object") {
    // strs 被收窄为 string[] | null,不是 string[]!
    // 因为 typeof null === "object"
    for (const s of strs) { // ❌ strs 可能是 null
      console.log(s);
    }
  }
}

这是 JavaScript 的历史遗留问题。解决方案:先用 truthiness 检查干掉 null

truthiness 收窄

JavaScript 中以下值在条件判断里被视为 false0NaN""(空字符串)、0nnullundefined。TypeScript 利用这一点做收窄:

function printAll(strs: string | string[] | null) {
  if (strs) {
    // strs 的真值检查排除了 null 和 undefined
    // 但空字符串 "" 也被排除了!
  }
}

一个更安全的版本——只排除 nullundefined

function printAll(strs: string | string[] | null) {
  if (strs && typeof strs === "object") {
    // strs 一定是 string[]
    for (const s of strs) {
      console.log(s);
    }
  } else if (typeof strs === "string") {
    console.log(strs);
  }
}

!= null 的妙用

JavaScript 里 value != null 同时排除了 nullundefined== null 的特殊行为),TypeScript 也识别这个模式:

interface Container {
  value: number | null | undefined;
}

function multiplyValue(container: Container, factor: number) {
  if (container.value != null) {
    // value 被收窄为 number
    container.value *= factor;
  }
}

equality 收窄

===!====!= 以及 switch 语句做类型收窄:

function example(x: string | number, y: string | boolean) {
  if (x === y) {
    // x 和 y 都是 string——这是它们唯一的共同类型
    x.toUpperCase();
    y.toLowerCase();
  }
}

利用字面量做收窄也很常见:

type Status = "loading" | "success" | "error";

function handleStatus(status: Status) {
  if (status === "loading") {
    console.log("加载中...");
  } else if (status === "success") {
    console.log("成功");    // status 收窄为 "success"
  } else {
    console.log("失败");    // status 收窄为 "error"
  }
}

switch 语句同理:

function handleStatus(status: Status) {
  switch (status) {
    case "loading":
      return "加载中";
    case "success":
      return "成功";
    case "error":
      return "失败";
  }
}

instanceof 收窄

instanceof 检查对象的构造函数,用于区分不同的类实例:

class Dog {
  bark() { console.log("汪"); }
}

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

function makeSound(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    animal.bark(); // animal 收窄为 Dog
  } else {
    animal.meow(); // animal 收窄为 Cat
  }
}

instanceof 只能用于类,不能用于 interfacetype——因为它们在运行时不存在。

in 操作符收窄

in 操作符检查对象是否包含某个属性,TypeScript 根据检查结果收窄联合类型:

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    animal.swim(); // animal 收窄为 Fish
  } else {
    animal.fly();  // animal 收窄为 Bird
  }
}

in 收窄特别适合区分两个”不共享同一个字面量字段”的对象类型。比 typeofinstanceof 更灵活,因为不需要类或原始类型。

可选属性的影响

如果属性是可选的,它会在 in 的两个分支都出现:

type Human = { swim?: () => void; fly?: () => void };

function move(animal: Fish | Bird | Human) {
  if ("swim" in animal) {
    // animal: Fish | Human(Human 也可能有 swim)
  } else {
    // animal: Bird | Human(Human 也可能没有 swim)
  }
}

控制流分析

TypeScript 不只是看 if 条件,它会分析整个执行路径。如果某个分支已经 return 了,编译器知道之后的代码绝对到不了那个分支的状态:

function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") {
    return " ".repeat(padding) + input;
  }
  // 能执行到这里,说明 padding 一定不是 number
  // 所以 TS 自动把 padding 收窄为 string
  return padding + input;
}

这叫控制流分析(Control Flow Analysis),它让编译器在你写了 returnthrow 后自动排除已处理的分支。

小结

  • 类型收窄让 TypeScript 在条件分支中自动确定更精确的类型
  • typeof 收窄是主力,但注意 typeof null === "object" 的坑
  • truthiness 收窄用 if (x) 排除 null/undefined,但也会误伤空字符串和 0
  • equality 收窄配合字面量和 switch 非常好用
  • instanceof 用于类实例判断
  • in 操作符通过检查属性是否存在来收窄联合类型
  • 控制流分析让编译器理解 return / throw 后的执行路径