12 常见报错排查与日常速查表
先问四个问题:实际值是什么?TS 推断是什么?目标契约是什么?是哪条路径/操作无法证明兼容?
1. string 不能赋给某个联合类型
典型原因:可变对象/数组把具体值扩大为 string,或真实外部输入确实可能是任意字符串。
选择:固定配置用 as const/satisfies,明确可变数组用 Status[];外部字符串先校验。不要把所有 string 都直接 as Status。
2. readonly 数组不能赋给可变数组
目标函数可能会 push 或修改。只读输入不能承诺能被修改。
自己的读取函数改为 readonly T[];库确实需要可变数组时复制或按库类型重新构造。不要用断言把“不会修改”的事实变成虚假承诺。
嵌套数组只复制外层仍可能含只读项,Order 等结构建议直接按明确类型构造。
3. 对象可能为 null / undefined
常见位置:findByPk、数组首项、环境变量、关联、可选字段。
先确认缺失是否合理:合理就表达可选并处理;不合理就检查后抛错。! 不会加载关联或读取 .env。
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 链没链接。
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. 遇到报错的五步法
- 查看变量实际推断,不先改配置。
- 写出目标契约的含义,判断是否应该调整目标。
- 确认数据来源与执行路径,找到缺少的证据。
- 在正确边界增加类型标注、校验、转换或收窄。
- 对关键输入/查询补测试,而不只满足编辑器。
15. 综合练习
建立一份固定客户排序配置,派生键和值类型;校验未知排序输入;构造已规范化分页;查询后转换名字/状态 DTO;所有公共函数明确返回值;为无效键、缺失页码、active:false、空结果各写一个测试。
涉及数据库写入与并发的练习使用独立测试库。类型测试与运行时测试相互补充,不互相替代。
返回:系列目录。