16 血泪速查

按症状索引的踩坑表:你这么写 / 实际发生什么 / 正确写法

16 血泪速查

这一页按症状索引,不是按语法索引。出问题时从「我看到的现象」出发查,比从「这是哪个语法」出发快得多。

本页所有「实际发生什么」都在 TS 6.0.3 实测过。


类型没报错,但运行时炸了

这一类是最危险的——编译器全程绿灯,生产环境报错。

你这么写实际发生什么正确写法
const u: User = await res.json()any 静默赋给 User,零校验schema 校验(见 11)🔥
const d = JSON.parse(s)返回 any,之后全无检查schema.parse(JSON.parse(s))
const x: User = raw as User断言不检查,运行时可能缺字段类型守卫 / schema
localStorage.getItem(k)返回 string | null,JSON.parse 后是 any校验后再用
process.env.PORT 声明成 string运行时可能是 undefined启动时 schema 校验
el!.focus()! 只去掉类型,不保证非空判空或可选链
arr[0] 当 T 用越界/空数组时是 undefined开 noUncheckedIndexedAccess
Object.keys(obj) 当 (keyof T)[]实际是 string[]手写 as (keyof T)[] 或改数据结构
1
2
3
4
5
6
// 🛑 三行全绿,运行时炸
async function loadUser(id: string) {
  const res = await fetch(`/api/users/${id}`);
  const user: User = await res.json();       // ✅ 编译通过
  return user.name.toUpperCase();            // 💥 若响应是 { username: ... }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// ✅ 守住边界
import { z } from "zod";

const UserSchema = z.object({ id: z.number(), name: z.string() });
type User = z.infer<typeof UserSchema>;

async function loadUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  const raw: unknown = await res.json();
  return UserSchema.parse(raw);              // ✅ 真检查
}

🔥 记住这一条:类型注解不是运行时保证。凡是从边界进来的数据,编译器的绿灯毫无意义。


null / undefined 相关

你这么写实际发生什么正确写法
if (n) 判断 number | null0 也被排除,走了错误分支if (n !== null)
if (s) 判断 string | undefined空字符串也被排除if (s !== undefined)
a || b 提供默认值0 / "" / false 被替换掉用 a ?? b 🔥
obj.prop.toUpperCase()TS18048 / TS18047obj.prop?.toUpperCase()
收窄后在回调里用收窄失效,TS18047先存 const
收窄后重新赋值收窄作废别在收窄后改该变量
ref.current.focus()useRef 初始为 nullref.current?.focus()
可选属性当必填用可能是 undefined判空或改类型
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 🛑 真值判断吞掉合法值
function retry(times: number | null) {
  if (times) return times * 2;    // times = 0 时走 else!
  return 0;
}

// ✅ 显式判断
function retry2(times: number | null) {
  return times !== null ? times * 2 : 0;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 🛑 回调里收窄失效
function bad(v: string | null) {
  if (v !== null) {
    setTimeout(() => v.toUpperCase(), 0);
    //                ~ TS18047: 'v' is possibly 'null'.
  }
}

// ✅ 先存 const
function good(v: string | null) {
  if (v !== null) {
    const s = v;
    setTimeout(() => s.toUpperCase(), 0);   // ✅
  }
}
运算符只对 null/undefined 生效
??✅ 保留 0、""、false 🔥
||❌ 会替换掉它们
?.✅ 只在 null/undefined 时短路

类型不符合直觉

你这么写实际发生什么正确写法
const x: {} = "a"合法!{} 不是「空对象」用 object 或具体形状
const x: object = "a"TS2322,原始类型不符合 object明确要用哪个
typeof v === "object" 判非空对象null 也满足加 && v !== null 🔥
typeof arr === "array"永远 falseArray.isArray(arr)
x instanceof SomeInterface编译失败(接口运行时不存在)自定义守卫
readonly obj.prop.deep 改内层合法!readonly 是浅层的DeepReadonly(见 06)
keyof Dict(含索引签名)结果是 string | number ⚠️string & keyof Dict
Omit<T, "typo">不报错,静默不排除StrictOmit
交叉同名属性属性类型变 never避免同名,或显式 Omit
readonly T[] 赋给 T[]TS4104[...ro] 复制
用 String / Number包装对象类型,几乎总是写错小写 string / number
1
2
3
4
// 🛑 三个都合法,但含义差很远
const a: {} = "字符串可以";        // ✅ 合法
const b: object = "字符串不行";    // ❌ TS2322
const c: object = () => {};        // ✅ 函数也是 object
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 🛑 typeof null 是 "object"(JS 历史 bug)
function f(v: string | null) {
  if (typeof v === "object") {
    return "null 也会进来!";
  }
  return v;
}

// ✅ 正确判非空对象
if (typeof v === "object" && v !== null) { /* ... */ }

泛型与推断

你这么写实际发生什么正确写法
期望从返回值推断 TT 变成 unknown显式 f<string>()
first([])T 推断为 neverfirst<string>([])
条件类型用在联合上分配律导致结果变联合[T] extends [U] ? ... 🔥
IsNever<T> 判 never结果是 never 不是 true[T] extends [never]
映射类型丢修饰符? / readonly 消失用同态形式 [K in keyof T]
Capitalize<K> 报错K 可能是 number/symbolCapitalize<string & K>
递归类型太深TS2589加深度计数器
默认值参数参与推断联合被意外扩大NoInfer<T> 🆕
as const 忘了写推断成 string 而非字面量常量表加 as const
重载顺序反了推断成宽类型窄签名写前面
<T> 在 .tsx 里TS17008<T,> 或 <T extends unknown>
1
2
3
4
5
6
7
// 🛑 分配律:期望整体判断,结果逐成员判断
type IsString<T> = T extends string ? true : false;
type A = IsString<string | number>;   // boolean(不是 false!)

// ✅ 用元组阻止分配
type IsString2<T> = [T] extends [string] ? true : false;
type B = IsString2<string | number>;  // false

模块与导入

你这么写实际发生什么正确写法
nodenext 下 import "./x"TS2835 缺扩展名写 "./x.js"(不是 .ts)🔥
类型没写 import typeTS1484import type { X }
export { SomeType }TS1205export type { SomeType }
.d.ts 无顶层 import/export成为脚本,污染全局 ⚠️加 export {}
在模块文件里写 declare module "*.css"TS2664放到无 import 的脚本文件 🔥
以为 paths 影响运行时打包器/Node 找不到模块同步配 resolve.alias 或 imports
6.0 下找不到 processtypes 默认 []types: ["node"]
用 baseUrlTS5101 废弃前缀写进 paths
用 moduleResolution: "node"TS5107 废弃"bundler" 或 "nodenext"
1
2
3
// 🛑 这个文件没有 import/export,是「脚本」
interface Config { url: string }
// ↑ 这是全局声明!会污染整个项目,且可能与其他文件冲突
1
2
3
4
// ✅ 加一行让它成为模块
export {};                        // 或 import type {} from "x"

interface Config { url: string }  // 现在只在本文件内可见

类与对象

你这么写实际发生什么正确写法
private 当安全边界运行时仍可访问用 #field 🔥
类属性没初始化TS2564给初值 / 构造函数赋值 / ?
滥用 ! 明确赋值断言运行时是 undefined老实初始化
{ a: 1, b: 2 } 赋给单属性类型TS2353 多余属性检查经变量中转,或改类型
在 function 回调里用 thisTS2683,this 丢失用箭头函数
忘了 override基类改名后静默失联开 noImplicitOverride
构造函数参数属性生成运行时代码,Node 直跑报错手写字段赋值(erasableSyntaxOnly 下必需)
索引签名加已知属性TS2411 类型不兼容统一类型
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 🛑 private 只是编译期
class Secret {
  private key = "abc";
}
const s = new Secret();
(s as any).key;              // ✅ 运行时拿得到
s["key"];                    // ✅ 也拿得到

// ✅ # 是运行时真私有
class Secret2 {
  #key = "abc";
  getKey() { return this.#key; }
}
const s2 = new Secret2();
(s2 as any).#key;            // ❌ 语法错误
Object.keys(s2);             // [] —— 完全看不到

枚举与常量

你这么写实际发生什么正确写法
enum 做状态码生成运行时代码,Node 直跑报错as const 对象 + 联合类型 🔥
const enum 跨包使用值被内联,isolatedModules 环境下可能取不到用 as const 对象
enum 当类型又当值两个空间都有,容易混淆明确分开类型与常量
手写联合类型容易漏、容易过时(typeof ARR)[number]
1
2
3
4
5
6
7
8
9
// 🛑 enum:生成运行时代码,且不能通过 erasableSyntaxOnly
export enum Status { Idle = "idle", Done = "done" }

// ✅ 推荐:as const 对象
export const Status = {
  Idle: "idle",
  Done: "done",
} as const;
export type Status = (typeof Status)[keyof typeof Status];   // "idle" | "done" 🔥
方案运行时代码Node 直跑类型安全
enum✅ 有❌✅
const enum❌ 内联⚠️ 视环境✅
as const 对象✅ 有(普通对象)✅✅ 🔥
字符串字面量联合❌ 无✅✅

数组与集合

你这么写实际发生什么正确写法
string[] 赋给 (string | number)[]合法但不健全,写入后类型污染参数用 readonly T[]
readonly 数组上 .sort() / .reverse()TS2339[...arr].sort()
.sort() 以为返回新数组就地修改原数组先复制
.filter() 期望自动收窄类型没变窄手写 (x): x is T =>
arr[0] 当 T可能 undefined开 noUncheckedIndexedAccess
.find() 结果直接用可能是 undefined判空
1
2
3
4
5
// 🛑 数组协变的不健全漏洞
let strs: string[] = ["a"];
let anys: (string | number)[] = strs;   // ✅ 编译通过
anys.push(123);                          // ✅ 也通过
strs[1].toUpperCase();                   // 💥 运行时崩溃
1
2
3
4
5
// ✅ 防御:函数参数用 readonly
function sum(xs: readonly number[]): number { /* ... */ }

// ✅ 或复制后再操作
const sorted = [...items].sort((a, b) => a - b);

any 与抑制指令

你这么写实际发生什么正确写法
as any 让编译通过any 会传染下游用 unknown + 收窄
@ts-ignore错了也不提示,永久留存@ts-expect-error 🔥
@ts-expect-error 没用了TS2578 报错(这是好事)删掉它
@ts-nocheck 整个文件新代码也失去保护登记 + 期限
无类型的 declare module "x"等于没有类型最小声明,参数用 unknown
catch (e) 当 Error 用TS18046instanceof Error 收窄
1
2
3
4
5
6
// 🛑 any 传染
declare const data: any;
const a = data.foo;         // any
const b = a.bar;            // any
function f(x: any) { return x.baz; }
const c = f(1);             // any —— 三跳之后依然是 any
1
2
3
4
5
// ✅ @ts-expect-error 会自我清理
// @ts-expect-error 上游库缺少该重载,见 issue #123
legacyCall(x);
// 上游修好后 → error TS2578: Unused '@ts-expect-error' directive.
// → CI 失败 → 你删掉它 ✅

异步

你这么写实际发生什么正确写法
忘了 awaitPromise 浮空,错误丢失@typescript-eslint/no-floating-promises
忘了 Awaited拿到 Promise<T> 而不是 TAwaited<ReturnType<typeof fn>>
setTimeout 标 numberNode 下是 NodeJS.TimeoutReturnType<typeof setTimeout>
以为 allSettled 的 reason 是 Error实际是 any ⚠️自己收窄
JSON.stringify(err)得到 {}(属性不可枚举)手动提取 message / stack
async 函数标 : numberTS1064标 : Promise<number>
1
2
3
4
5
6
7
8
9
// 🛑 Error 序列化成空对象
JSON.stringify(new Error("出错了"));   // "{}"

// ✅ 手动序列化
const serialized = {
  name: err.name,
  message: err.message,
  stack: err.stack,
};

tsconfig 与工程

你这么写实际发生什么正确写法
6.0 下不写 types@types/node 不加载types: ["node"]
期望 rootDir 自动推断产物多一层 src/显式 rootDir
extends 后期望 lib 合并数组选项是覆盖子配置写完整
开了 noUncheckedIndexedAccess 但关了 strictNullChecks静默失效必须开 strictNullChecks
以为 strict 含所有检查索引访问、exact optional 都不在内单独开启
以为 skipLibCheck 万能你自己的 .d.ts 也不检查了手写声明要额外仔细
命令行传文件 + 有 tsconfigTS5112加 --ignoreConfig
以为打包器会检查类型类型错误进主干CI 加 tsc --noEmit 🔥
dist 里有测试文件测试被编译进产物exclude 测试
1
2
3
4
5
6
7
8
9
// 🛑 数组选项不会合并
// tsconfig.base.json
{ "compilerOptions": { "lib": ["ES2023", "DOM", "DOM.Iterable"] } }

// tsconfig.json
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": { "lib": ["ES2023"] }   // ⚠️ DOM 全丢了!
}
1
2
3
4
5
// ✅ 写完整
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": { "lib": ["ES2023", "DOM", "DOM.Iterable"] }
}

快速自查清单

把这份清单贴进 code review checklist:

检查项为什么
所有外部数据都经过校验了吗?类型在运行时不存在 🔥
有没有 as any / @ts-ignore?新增的技术债
索引访问判空了没?noUncheckedIndexedAccess
可选链 ?. 和 ?? 用对了吗?别用 || 做默认值
新代码有没有加 as const?常量表才能推出字面量联合
公开 API 用 interface 还是 type?影响使用者能否扩充
有没有在收窄后重新赋值?收窄会失效
readonly 是浅的,深层改了吗?可能需要 DeepReadonly
异步函数漏 await 了吗?用 lint 规则兜住
CI 里有 tsc --noEmit 吗?打包器不检查类型 🔥
最后修改 September 20, 2026: 更新 (25684a4ed)