TypeScript 在执行前检查一部分程序约束,生成的 JavaScript 通常不保留这些类型。给接口返回值写一个类型断言,不会把不合法的 JSON 变合法。把编译期表达能力与运行时验证分开,才不会出现“检查通过却在浏览器崩溃”。

让不可能的状态更难表达

分别声明 loading、error、data 三个可独立变化的变量,会出现“加载中但旧错误仍显示”等组合。使用可辨识联合类型,可以让界面按状态逐一处理。

type Load<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; message: string };

function describe(state: Load<string[]>): string {
  switch (state.status) {
    case "idle": return "尚未查询";
    case "loading": return "查询中";
    case "success": return "共 " + state.data.length + " 项";
    case "error": return state.message;
  }
}

访问 status 后,编译器会收窄可能的成员。泛型 T 保留输入输出之间的关系,不是给所有东西换成 any。unknown 表示尚未证实的值,使用前必须检查;any 会跳过大量保护,应限制在确有必要的适配边界。

从未知数据建立可信值

type Lesson = { id: string; minutes: number };

function parseLesson(value: unknown): Lesson {
  if (typeof value !== "object" || value === null ||
      !("id" in value) || typeof value.id !== "string" ||
      !("minutes" in value) || typeof value.minutes !== "number" ||
      !Number.isFinite(value.minutes) || value.minutes < 0) {
    throw new Error("学习记录格式不正确");
  }
  return { id: value.id, minutes: value.minutes };
}

这个检查主动重建对象,明确保留哪些字段。真实协议还需要约束长度、版本、枚举和嵌套结构;可以采用经过评估的 schema 库,仍要设计错误如何交给用户。编译器的结构类型意味着“具有所需字段”常常足够,并不自动检查对象只含这些字段。

项目开启 strict,尽量避免非空断言掩盖初始化顺序。noUncheckedIndexedAccess 可提醒数组和索引访问可能得不到值;exactOptionalPropertyTypes 用于更严格地区分缺字段与显式 undefined。设置会影响现有代码,应通过迁移逐步完成。

完整类型与验证示例 可用 TypeScript 执行无输出类型检查,再在支持类型擦除的 Node 中运行,或先编译再运行。运行时检查和 tsc 是两件事,不应只做其中一个。

练习:给 Lesson 加可选的 completedAt 字段,只接受合法的日期字符串或缺字段。再给 Load 增加 cancelled 状态,观察哪些 switch 需要补分支。检查输入不是对象、minutes 是字符串、NaN 或负数时的行为。

面试自查:unknown 与 any 有什么区别?类型断言和类型转换有什么区别?类型系统为什么不能代替服务端校验?

参考:TypeScript 手册、联合类型与收窄、TSConfig。