讲解

内置收窄手段(typeof、instanceof)应付不了「结构判别」:比如「这个 unknown 到底是不是 User 形状」。自定义类型守卫是返回类型为「值 is 类型」的函数:function isUser(v: unknown): v is User——函数体里做运行时检查,返回 true 的分支里编译器把 v 收窄为 User。它把「运行时检查」和「编译期收窄」焊在了一起,是处理外部数据(API 返回、localStorage、消息事件)的核心武器。

写类型守卫的原则:函数体内必须真的逐项检查(typeof、in、数组 every 组合),不能 body 里直接 return true——守卫函数就是运行时防线,它撒谎编译器会信。守卫可以组合:isUserArray 用 Array.isArray + every(isUser)。现代项目里这类逻辑常被 zod 等校验库替代(schema 一次定义,类型和校验同时得到),但手写守卫依然是理解类型系统与零依赖场景的必修课。

断言函数(assertion function)是守卫的命令式版本:返回类型写 asserts value is T,函数体里条件不满足就 throw。调用之后编译器直接认为值已经是 T——适合「不合格就崩溃」的初始化检查、配置校验。还有 asserts condition 形态(不指定值),Node 的 assert 就是这个类型。

示例

手写结构守卫:

interface User {
  id: number;
  name: string;
  admin: boolean;
}

function isUser(value: unknown): value is User {
  if (typeof value !== "object" || value === null) {
    return false;
  }
  const v = value as Record<string, unknown>;
  return typeof v.id === "number" && typeof v.name === "string" && typeof v.admin === "boolean";
}

function showUser(raw: unknown): string {
  if (isUser(raw)) {
    return `[${raw.admin ? "管理员" : "用户"}] ${raw.name}`;
  }
  return "(无效的用户数据)";
}

console.log(showUser({ id: 1, name: "小林", admin: true }));
console.log(showUser({ id: "oops" }));

组合守卫检查数组:

function isStringArray(value: unknown): value is string[] {
  return Array.isArray(value) && value.every((item) => typeof item === "string");
}

function joinTags(raw: unknown): string {
  if (isStringArray(raw)) {
    return raw.join("、");
  }
  return "(无标签)";
}

console.log(joinTags(["前端", "类型系统"]));
console.log(joinTags([1, 2]));

断言函数:不合格就直接抛错:

function assertDefined<T>(value: T, name: string): asserts value is NonNullable<T> {
  if (value === null || value === undefined) {
    throw new Error(`${name} 不能为空`);
  }
}

function boot(config: { apiBase?: string }): string {
  assertDefined(config.apiBase, "apiBase");
  return `API 地址:${config.apiBase}`; // 这里已经是 string
}

console.log(boot({ apiBase: "https://api.example.com" }));

常见坑

  • 守卫函数只检查一层:嵌套属性没检查,true 返回后深层取值照样崩——检查深度要和消费深度一致。
  • is 断言与实现不符:手写守卫没有任何编译器校验,实现写错(比如把 number 写成 string)编译器照单全收,务必配单元测试。
  • 用断言函数做流程分支:asserts 是「不合格就抛」的硬门槛;需要优雅降级时用返回 boolean 的守卫 + if。
  • 重复发明轮子:项目里守卫超过三四个就该评估 zod/valibot——一次 schema 定义同时得到校验和类型。

小结

v is T 守卫把运行时检查接到编译期收窄;asserts 断言函数用于硬性前置校验;两者都是外部数据的入口防线。下一章讲模块系统如何与类型协作。