12 常见报错排查与日常速查表

先问四个问题:实际值是什么?TS 推断是什么?目标契约是什么?是哪条路径/操作无法证明兼容?

1. string 不能赋给某个联合类型

典型原因:可变对象/数组把具体值扩大为 string,或真实外部输入确实可能是任意字符串。

选择:固定配置用 as const/satisfies,明确可变数组用 Status[];外部字符串先校验。不要把所有 string 都直接 as Status。

见 01、02。

2. readonly 数组不能赋给可变数组

目标函数可能会 push 或修改。只读输入不能承诺能被修改。

自己的读取函数改为 readonly T[];库确实需要可变数组时复制或按库类型重新构造。不要用断言把“不会修改”的事实变成虚假承诺。

嵌套数组只复制外层仍可能含只读项,Order 等结构建议直接按明确类型构造。

3. 对象可能为 null / undefined

常见位置:findByPk、数组首项、环境变量、关联、可选字段。

先确认缺失是否合理:合理就表达可选并处理;不合理就检查后抛错。! 不会加载关联或读取 .env。

见 05、07。

4. Property does not exist

const input = { first_name: "ANN" };
input.email = "ann@example.com"; // 错误:推断形状没有 email

若本来会逐步添加字段,先定义真实契约:

interface Input { first_name: string; email?: string }
const input: Input = { first_name: "ANN" };
input.email = "ann@example.com";

若拼错字段,修拼写;若只是查询别名/关联,检查是否加载及是否需独立结果类型。不要为了访问一个属性就扩大整个对象成 Record<string, unknown>。

5. 任意 string 不能索引固定对象

const labels = { active: "启用", banned: "禁用" };

function readLabel(key: string) {
  return labels[key]; // 错误:key 可能不是合法键
}

内部受控调用把参数限制为 keyof typeof labels;外部输入先做自身键白名单检查。真正开放的字典可用 Record<string, T>,但读取时要处理 undefined。

见 02 章。

6. Object.keys 为什么返回 string[]?

interface CustomerName { first_name: string }
const full = { first_name: "ANN", private_note: "internal" };
const view: CustomerName = full;

Object.keys(view); // 实际有 first_name 与 private_note

静态类型可能只是更大对象的一个视图。Object.keys 枚举运行时自身可枚举键,不能普遍保证仅包含 keyof 静态类型。

对程序自己定义且键集合受控的固定配置,可以使用局部断言,并说明依据:

const sort_columns = { id: "customer_id", name: "last_name" } as const;
const keys = Object.keys(sort_columns) as Array<keyof typeof sort_columns>;
for (const key of keys) console.log(sort_columns[key]);

不要把这包装成对任意对象都承诺精确键类型的通用工具。需要固定字段清单时直接写 as const 数组;只是遍历键值时 Object.entries 可能更直观,但也要观察其推断类型。

7. 对象字面量报额外字段,变量赋值却通过?

interface CustomerName { first_name: string }

const direct: CustomerName = {
  first_name: "ANN",
  private_note: "internal", // 新鲜对象字面量额外属性检查
};

const full = { first_name: "ANN", private_note: "internal" };
const indirect: CustomerName = full; // 通常允许,结构满足必要字段

TS 主要是结构化类型系统,不是每次赋值都要求键集合完全相等。额外属性检查帮助发现拼写错误,但不是数据安全过滤机制。

Pick/Omit/satisfies 也不普遍提供“运行时删除额外字段”。真正输出/写入白名单需要构造新对象。

8. Omit 拼错字段名为什么可能不报错?

interface CustomerName { first_name: string; last_name: string }
type Result = Omit<CustomerName, "frist_name">;

标准 Omit 的键参数允许 PropertyKey,不一定限制为 keyof T,因此拼错可能只是“没排除任何字段”。Pick 对不存在的键通常会报错。

保持简单字段白名单,重要类型用检查/测试复盘;不要认为用了工具类型就自动验证所有业务意图。

9. catch 中 error 为什么是 unknown?

JS 可以 throw 任意值,不只 Error:

try {
  throw "unexpected";
} catch (error: unknown) {
  const message = error instanceof Error ? error.message : "未知异常";
  console.error(message);
}

使用实例检查后访问 message;当前项目已知 AppError、Sequelize 错误等可以按 instanceof 分类。

不要为了读取 message 把 error 改成 any,也不要把所有内部 message 直接返回给用户。日志、错误脱敏和业务响应仍有各自规则。

10. unknown、any 与 never 怎么记?

类型 大意 适合用途
unknown 有值,但还不知道结构 外部输入、JSON、未知异常,检查后使用
any 大幅绕过类型检查 明确演示类型边界时谨慎使用
never 不可能的值/不会正常返回 穷尽检查、总是抛错的路径

unknown 不是“更麻烦的 any”,它要求你在使用前给出证据。never 也不是“未知”;它是已经没有可能值。

11. 函数缺少返回值

检查:某个 if 没覆盖、catch 吞错、回调 return 不返回外层、async Promise 链没链接。

根据业务选择默认返回、抛错或明确可选返回,见 06、09。

12. 类型通过,运行仍失败

常见原因:请求没校验,SQL 泛型声明错,未 include,mapper 取错同类型字段,忘 await,事务可见性问题。

先看真实数据与执行顺序,再检查类型。增加断言不会修复运行时事实,见 10 章。

13. 日常操作速查

想做什么 常用工具 它不替你做什么
固定值集合派生类型 as const + typeof + [number] 校验外部字符串
配置结构检查 satisfies 冻结对象、读取数据库
属性名范围 keyof 运行时枚举 interface
只选/排除字段类型 Pick / Omit 删除真实对象字段
可选更新输入 Partial + 白名单 保证至少一项、校验值
固定键文案映射 Record<联合, 值类型> 创建默认值
缺失检查 if、守卫、asserts 自动加载关联
无漏处理分支 switch + never 校验未可信输入
复用函数签名 ReturnType / Parameters / Awaited 执行/等待函数
类型专用依赖 import type 完成运行时初始化

14. 遇到报错的五步法

  1. 查看变量实际推断,不先改配置。
  2. 写出目标契约的含义,判断是否应该调整目标。
  3. 确认数据来源与执行路径,找到缺少的证据。
  4. 在正确边界增加类型标注、校验、转换或收窄。
  5. 对关键输入/查询补测试,而不只满足编辑器。

15. 综合练习

建立一份固定客户排序配置,派生键和值类型;校验未知排序输入;构造已规范化分页;查询后转换名字/状态 DTO;所有公共函数明确返回值;为无效键、缺失页码、active:false、空结果各写一个测试。

涉及数据库写入与并发的练习使用独立测试库。类型测试与运行时测试相互补充,不互相替代。

返回:系列目录。