TypeScript: TypeScript 类型守卫与收窄

最后更新:2026-08-26

类型收窄是 TypeScript 最实用的特性之一——把宽泛的类型缩小到更精确的范围,安全地访问特定类型的属性和方法。

1. 类型收窄概述

联合类型的变量只能访问所有成员共有的属性。类型收窄让我们在特定代码分支中"收窄"类型,访问该分支独有的成员:

TYPESCRIPT
function process(value: string | number) {
  // 收窄前——value 只能用共有方法
  console.log(value.toString());  // ✅ string 和 number 都有

  // 收窄后——value 可以用特定方法
  if (typeof value === "string") {
    console.log(value.toUpperCase());  // ✅ string 独有方法
  } else {
    console.log(value.toFixed(2));     // ✅ number 独有方法
  }
}

TypeScript 支持以下收窄方式:

收窄方式 适用场景
typeof 基本类型判断
instanceof 类实例判断
in 操作符 属性是否存在
相等性检查 ===/!== 比较
自定义类型守卫 复杂类型判断
可赋值收窄 赋值时自动收窄

2. typeof 收窄

typeof 最常用于区分基本类型:

(1) 基本用法

TYPESCRIPT
function padLeft(value: string, padding: string | number): string {
  if (typeof padding === "number") {
    return " ".repeat(padding) + value;    // padding 收窄为 number
  }
  return padding + value;                   // padding 收窄为 string
}

console.log(padLeft("hello", 4));      // "    hello"
console.log(padLeft("hello", ">>>"));  // ">>>hello"

(2) typeof 的返回值

TYPESCRIPT
typeof "hello"     // "string"
typeof 42          // "number"
typeof true        // "boolean"
typeof undefined   // "undefined"
typeof Symbol()    // "symbol"
typeof 100n        // "bigint"
typeof {}          // "object"   ⚠️ 包括 null、数组、Date 等
typeof function(){} // "function"
🔥 易错: typeof null === "object" 是 JavaScript 的历史 bug。用 typeof 无法区分 null 和其他对象——需要用 === null 判断。

(3) typeof 与 switch

TYPESCRIPT
function describe(value: string | number | boolean | undefined) {
  switch (typeof value) {
    case "string":
      return `字符串:${value.toUpperCase()}`;
    case "number":
      return `数字:${value.toFixed(2)}`;
    case "boolean":
      return `布尔:${value}`;
    case "undefined":
      return "未定义";
  }
}

3. instanceof 收窄

instanceof 检查对象是否是某个类的实例——适合引用类型的收窄:

(1) 基本用法

TYPESCRIPT
function formatValue(value: Date | string | Error): string {
  if (value instanceof Date) {
    return value.toISOString();         // Date 方法
  } else if (value instanceof Error) {
    return value.message;               // Error 属性
  } else {
    return value.toUpperCase();         // string 方法
  }
}

console.log(formatValue(new Date()));          // "2024-..."
console.log(formatValue(new Error("出错")));   // "出错"
console.log(formatValue("hello"));             // "HELLO"

(2) instanceof 的限制

instanceof 只能用于类实例——不能用于 interface 或 type 别名(它们在运行时不存在):

TYPESCRIPT
interface Dog { bark(): void; }
interface Cat { meow(): void; }

// ❌ instanceof 不能用于接口
// if (pet instanceof Dog) { ... }

// ✅ 需要用自定义类型守卫或 in 操作符

(3) 自定义类与 instanceof

TYPESCRIPT
class NetworkError extends Error {
  constructor(public statusCode: number) {
    super(`网络错误:${statusCode}`);
  }
}

class ValidationError extends Error {
  constructor(public field: string) {
    super(`验证错误:${field}`);
  }
}

function handleError(error: NetworkError | ValidationError): string {
  if (error instanceof NetworkError) {
    return `HTTP ${error.statusCode} 错误`;
  } else {
    return `字段 ${error.field} 无效`;
  }
}

4. in 操作符收窄

in 检查对象是否有某个属性——适合区分接口/类型别名:

(1) 基本用法

TYPESCRIPT
interface Fish {
  swim(): void;
}

interface Bird {
  fly(): void;
}

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    animal.swim();   // ✅ Fish 类型
  } else {
    animal.fly();    // ✅ Bird 类型
  }
}

(2) 可辨识属性

TYPESCRIPT
interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape): number {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2;   // ✅ Circle 的 radius
  } else {
    return shape.sideLength ** 2;          // ✅ Square 的 sideLength
  }
}

▶ 示例:类型安全的事件处理

TYPESCRIPT
interface ClickEvent {
  type: "click";
  x: number;
  y: number;
}

interface KeyEvent {
  type: "keydown" | "keyup";
  key: string;
  ctrlKey: boolean;
}

interface ScrollEvent {
  type: "scroll";
  scrollTop: number;
  scrollLeft: number;
}

type UIEvent = ClickEvent | KeyEvent | ScrollEvent;

function handleEvent(event: UIEvent): string {
  switch (event.type) {
    case "click":
      return `点击位置:(${event.x}, ${event.y})`;
    case "keydown":
    case "keyup":
      return `按键:${event.key},Ctrl:${event.ctrlKey}`;
    case "scroll":
      return `滚动:top=${event.scrollTop}, left=${event.scrollLeft}`;
  }
}

console.log(handleEvent({ type: "click", x: 100, y: 200 }));
console.log(handleEvent({ type: "keydown", key: "Enter", ctrlKey: false }));
console.log(handleEvent({ type: "scroll", scrollTop: 50, scrollLeft: 0 }));
▶ 试一试

输出:

TEXT 📖 仅展示
点击位置:(100, 200)
按键:Enter,Ctrl:false
滚动:top=50, left=0

5. 自定义类型守卫

当内置收窄方式不够用,可以写自定义的类型守卫函数:

(1) 类型谓词(Type Predicate)

TYPESCRIPT
interface Dog {
  bark(): void;
  breed: string;
}

interface Cat {
  meow(): void;
  color: string;
}

// 类型谓词:返回值是 "参数名 is 类型"
function isDog(animal: Dog | Cat): animal is Dog {
  return "bark" in animal;
}

function interact(animal: Dog | Cat) {
  if (isDog(animal)) {
    animal.bark();    // ✅ 收窄为 Dog
    console.log(animal.breed);
  } else {
    animal.meow();    // ✅ 收窄为 Cat
    console.log(animal.color);
  }
}

(2) 断言函数(Assertion Function)

断言函数在条件不满足时抛异常——告诉 TypeScript "如果这行执行了,条件一定成立":

TYPESCRIPT
function assertDefined<T>(value: T | undefined | null, message?: string): asserts value is NonNullable<T> {
  if (value == null) {
    throw new Error(message ?? "值不能为 null 或 undefined");
  }
}

function processUser(user: User | undefined) {
  assertDefined(user, "用户不存在");
  // 此后 user 类型收窄为 User(不含 undefined)
  console.log(user.name.toUpperCase());  // ✅ 安全
}

(3) 断言 vs 类型谓词

特性 类型谓词 x is T 断言函数 asserts x is T
返回值 boolean void(不满足时抛异常)
使用方式 if (isType(x)) assertIsType(x)
类型收窄 在 if 分支收窄 调用后自动收窄
适合场景 检查后分支处理 前置条件检查

6. 可赋值收窄

赋值操作也会收窄类型:

TYPESCRIPT
let value: string | number;

value = "hello";
console.log(value.toUpperCase());  // ✅ 赋值后收窄为 string

value = 42;
console.log(value.toFixed(2));     // ✅ 赋值后收窄为 number

(1) 控制流分析

TypeScript 跟踪变量的类型在控制流中的变化:

TYPESCRIPT
function example(x: string | number | boolean) {
  // x: string | number | boolean
  if (typeof x === "string") {
    // x: string
    console.log(x.toUpperCase());
  } else {
    // x: number | boolean
    if (typeof x === "number") {
      // x: number
      console.log(x.toFixed(2));
    } else {
      // x: boolean
      console.log(x);
    }
  }
}

(2) 收窄与重新赋值

TYPESCRIPT
let value: string | number;

value = "hello";
console.log(value.length);  // ✅ string

value = 42;
// console.log(value.length);  // ❌ number 没有 length

value = true;  // ❌ boolean 不在联合类型中

7. 穷尽检查

确保 switch/if 覆盖所有可能的类型——用 never 类型保证完整性:

TYPESCRIPT
type Shape = "circle" | "square" | "triangle";

function getIcon(shape: Shape): string {
  switch (shape) {
    case "circle": return "○";
    case "square": return "□";
    case "triangle": return "△";
    default: {
      // 如果所有 case 都处理了,shape 在这里是 never
      const _exhaustive: never = shape;
      return _exhaustive;
    }
  }
}

// 如果以后 Shape 新增了 "hexagon" 但没加 case
// default 分支的 _exhaustive 会报类型错误
// 这提醒你补全遗漏的 case

(1) 更简洁的穷尽检查

TYPESCRIPT
function assertNever(value: never): never {
  throw new Error(`未处理的值:${value}`);
}

type Action = "create" | "update" | "delete";

function handleAction(action: Action) {
  switch (action) {
    case "create": /* ... */ break;
    case "update": /* ... */ break;
    case "delete": /* ... */ break;
    default:
      assertNever(action);  // 如果遗漏 case,这里会报类型错误
  }
}

❓ 常见问题

Q typeof 和 instanceof 有什么区别?
A typeof 检查值的"基本类型"(string/number/boolean/undefined/object/function),返回字符串,适合基本类型判断。instanceof 检查值是否是某个类的实例,适合引用类型判断(Date、Error、自定义类等)。两者互补,不是替代关系。
Q 自定义类型守卫有什么性能影响?
A 没有。类型守卫只在编译时使用——编译后的 JavaScript 就是普通的 if 判断和属性检查。类型谓词(x is T)和断言函数(asserts x is T)在运行时不存在,零开销。
Q 为什么 in 操作符能收窄类型?
A 因为 TypeScript 知道——如果对象有某个属性,那它一定属于包含该属性的接口。"swim" in animal 为 true 时,animal 一定实现了包含 swim 的接口。这是逻辑推理,不需要运行时类型信息。
Q 什么时候需要写自定义类型守卫?
A 当内置收窄方式(typeof/instanceof/in/===)无法区分类型时。最常见的是区分 interface——interface 在运行时不存在,无法用 instanceof 判断,只能用 in 检查可辨识属性或写自定义类型守卫。

📖 小节

📝 作业

  1. 基础题(难度⭐):写一个函数 doubleOrRepeat(value: string | number) —— 如果 value 是 number 就乘2,如果是 string 就重复拼接一次(如 "hi" → "hihi")。用 typeof 收窄。
  2. 进阶题(难度⭐⭐):定义 Admin(hasPermission 方法)和 Guest(requestAccess 方法)两个接口,用可辨识属性 role 收窄,写一个函数根据 role 调用不同方法。
  3. 挑战题(难度⭐⭐⭐):写一个自定义类型守卫 isNonNull<T>(value: T | null | undefined): value is NonNullable<T>,然后在 filterNonNull<T>(arr: (T | null | undefined)[]): T[] 函数中使用它,过滤掉 null/undefined 并返回非空数组。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏