第 19 章:常见坑速查
学习目标
- 识别 TS 新手最常踩的 15 个坑
- 理解每个坑的根因和解决方案
- 建立 TS 调试思路
- 形成 TS 编码规范
一、坑速查表
| # | 坑 | 表现 |
|---|---|---|
| 1 | 滥用 any | 类型保护全失 |
| 2 | 忘了 strictNullChecks | null/undefined 到处传 |
| 3 | 函数表达式返回类型错写 | 推导成"值的类型" |
| 4 | 元组 push 突破长度 | 运行时破坏类型约束 |
| 5 | 对象字面量多余字段 | "extra property" 报错 |
| 6 | private 字段运行时可访问 | 以为私有其实能读 |
| 7 | const enum + isolatedModules | Vite 项目不能用 |
| 8 | 类型断言绕过类型检查 | as any 全无敌 |
| 9 | 接口和 type 混淆 | 重复定义报错 |
| 10 | Object.keys() 返回 string[] | 推导不到具体 key |
| 11 | 异步函数返回 Promise<any> | res.json() 没校验 |
| 12 | == 收窄失效 | 用了 != 而不是 !== |
| 13 | 装饰器没开 experimentalDecorators | 编译报语法错 |
| 14 | 路径别名只在 tsconfig 配了 | Vite/webpack 编译失败 |
| 15 | for...in 包含原型链 | 类型可能多出字段 |
二、详解 1:滥用 any
typescript
function process(data: any): void { // ❌
return data.foo.bar;
}
function process(data: unknown): void { // ✅
if (typeof data === 'object' && data !== null && 'foo' in data) {
// 收窄
}
}三、详解 2:strictNullChecks
typescript
function greet(name: string | null): string {
return `Hello, ${name.toUpperCase()}`;
// ❌ Object is possibly 'null'
}
function greet(name: string | null): string {
return `Hello, ${name!.toUpperCase()}`;
// ⚠️ ! 断言不安全,改用条件判断
}
function greet(name: string | null): string {
return `Hello, ${name?.toUpperCase() ?? 'Guest'}`;
// ✅
}四、详解 3:函数表达式返回类型
typescript
// ❌ 把返回类型写在函数表达式上
const add: number = (a: number, b: number) => a + b;
// 这其实是给变量加类型,但函数本身推导为 number(无法返回 number)
// ✅ 正确写法
const add: (a: number, b: number) => number = (a, b) => a + b;五、详解 4:对象字面量多余字段
typescript
interface User { name: string }
function greet(u: User): void { console.log(u.name); }
// ❌ 直接传字面量报错
greet({ name: 'Tom', age: 18 });
// ✅ 改成变量
const u = { name: 'Tom', age: 18 };
greet(u);六、详解 5:Object.keys 推断
typescript
interface User {
id: number;
name: string;
}
const user: User = { id: 1, name: 'Tom' };
const keys = Object.keys(user); // string[],不是 ('id' | 'name')[]
// 解决:用 keyof 断言
const keys = Object.keys(user) as Array<keyof User>;
keys.forEach(k => user[k]);⚠️ 坑 1:
Object.keys的返回类型 TS 没法做到精确,运行时 key 顺序也不保证,生产代码慎用。
七、详解 6:异步 JSON 解析
typescript
async function getUser(): Promise<User> {
const res = await fetch('/api/user');
return res.json(); // 实际是 Promise<any>,只是赋给了 Promise<User>
}
// ✅ 加 zod 校验
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
});
async function getUser(): Promise<User> {
const res = await fetch('/api/user');
const data = UserSchema.parse(await res.json());
return data;
}八、详解 7:装饰器配置
json
// tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true // 必须开
}
}不开启编译会报 Decorators are not valid here(TS 5.0+ 已把 experimental 标记为稳定部分,但旧项目仍需配置)。
九、详解 8:路径别名(双端)
json
// tsconfig.json
{
"compilerOptions": {
"paths": { "@/*": ["src/*"] }
}
}typescript
// vite.config.ts
import { defineConfig } from 'vite';
import path from 'path';
export default defineConfig({
resolve: {
alias: { '@': path.resolve(__dirname, 'src') },
},
});⚠️ 坑 2:
tsconfig的paths只影响类型检查,打包器必须单独配。
十、详解 9:for...in
typescript
interface User {
id: number;
name: string;
}
const user = Object.create({ extra: 'foo' }); // 原型链上有 extra
Object.assign(user, { id: 1, name: 'Tom' });
for (const key in user) {
console.log(key); // extra, id, name(包含原型链)
}十一、详解 10:== 收窄
typescript
function process(x: string | null): void {
if (x == null) { // ✅ 等价于 x === null || x === undefined
return;
}
x.toUpperCase();
}⚠️ 坑 3:项目里
==只在收窄 null/undefined 时使用,其他地方必须===/!==,ESLint 配eqeqeq: ['error', 'always', { null: 'ignore' }]。
十二、TS 调试技巧
bash
# 1. 看具体报错位置的上下文
tsc --pretty
# 2. 查看类型推导
tsc --traceResolution
# 3. 查看某个表达式的类型
import('typescript').then(ts => {
// 用 checker
});
# 4. VSCode:光标移到变量上,看 hover 类型十三、本章小结
| 调试思路 | 工具 |
|---|---|
| 报错信息 | 看红色波浪线 + VSCode Problems 面板 |
| hover 类型 | 把鼠标放到变量上看弹出类型 |
| tsc --traceResolution | 看模块解析路径 |
| 类型展开 | tsc --listFiles 看参与编译的文件 |
| 排查 any | 搜索 : any 看泄漏点 |
动手练习
- any 大扫除:找一个老项目,统计
any使用次数,改unknown看哪里需要适配 - null 守卫:把所有可能的 null/undefined 显式标注,加
?.或条件判断 - 配置核对:打开自己的
tsconfig.json,逐项对照本教程的推荐配置
下一章:第 20 章:进阶资源 →