最近 Code Review 了组里三个新人的代码,发现同样的问题反复出现。
不是逻辑错误——TypeScript 编译器会帮你抓。是那种能跑,但让接手的人想打人的写法。
总结了 8 个最常见的反模式。你可能正在写其中至少 3 个。
// ❌ 遇到类型报错就any
const handleResponse = (data: any) => {
return data.result.items.map((item: any) => item.name);
};
看着没问题。但 data 的结构变了呢?items 不存在了呢?name 改成 title 了呢?
TypeScript 不会告诉你——因为你告诉它"我不在乎类型"。
// ✅ 花30秒定义类型
interface ApiResponse {
result: {
items: Array<{ name: string; id: number }>;
};
}
const handleResponse = (data: ApiResponse) => {
return data.result.items.map((item) => item.name);
};
原则:每多一个 any,你的 TypeScript 就退化成了带类型注释的 JavaScript。
如果你真的不知道类型是什么——用 unknown,下一节说为什么。
// ❌ catch里用any
try {
await fetchData();
} catch (error: any) {
console.log(error.message); // 如果error不是Error对象呢?
console.log(error.response.status); // 如果没有response呢?
}
catch 的 error 可能是任何东西——不只是 Error 对象。有可能是字符串、null、甚至 undefined。
// ✅ 用unknown + 类型守卫
try {
await fetchData();
} catch (error: unknown) {
if (error instanceof Error) {
console.log(error.message);
}
if (isAxiosError(error)) {
console.log(error.response?.status);
}
}
unknown 强制你在使用前做类型检查——any 则让你假装知道它是什么。
// ❌ 到处用 as 强转
const user = response.data as User;
const element = document.getElementById('root') as HTMLDivElement;
const config = JSON.parse(text) as AppConfig;
as 的意思是"我比编译器更懂"。但你真的更懂吗?
如果 response.data 返回的不是 User 结构?如果那个 DOM 元素不存在或者不是 div?运行时崩溃,TypeScript 不会预警。
// ✅ 用类型守卫做运行时检查
function isUser(data: unknown): data is User {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
'name' in data
);
}
const data = response.data;
if (isUser(data)) {
// 这里 data 被收窄为 User,编译器和运行时都安全
console.log(data.name);
}
// DOM 元素用 instanceof
const element = document.getElementById('root');
if (element instanceof HTMLDivElement) {
element.style.display = 'flex';
}
原则:as 是骗编译器,类型守卫是让编译器帮你验证。
唯一合理用 as 的场景:你能 100% 确定类型,且加守卫的成本不值得(比如测试代码里 mock 数据)。
// ❌ 为了几个固定值搞个enum
enum Status {
Active = 'active',
Inactive = 'inactive',
Pending = 'pending',
}
enum Direction {
Up = 'up',
Down = 'down',
Left = 'left',
Right = 'right',
}
enum 看着很规范,但它有两个问题:
// ✅ union type:零运行时开销,类型提示一样好
type Status = 'active' | 'inactive' | 'pending';
type Direction = 'up' | 'down' | 'left' | 'right';
// 需要遍历所有值?用 const 数组 + typeof
const STATUSES = ['active', 'inactive', 'pending'] as const;
type Status = typeof STATUSES[number];
什么时候用 enum: 需要反向映射(数字→名字)、或者值需要作为对象使用(Status.Active)且团队统一约定用 enum。其他场景 union type 更轻量。
// ❌ 一路?.到底,每个属性都加
const name = user?.profile?.settings?.displayName?.trim()?.toLowerCase();
// name 的类型是 string | undefined
const items = data?.response?.result?.items?.filter(i => i?.active);
// items 的类型是 Item[] | undefined
可选链是好东西,但滥用它等于在说:"我不确定这个数据结构长什么样。"
结果:每个变量都可能是 undefined,下游代码全都要加空值检查,undefined 像传染病一样扩散。
// ✅ 在入口处做一次空值检查,内部使用确定类型
function renderProfile(user: User | null) {
if (!user) return <EmptyState />;
// 过了守卫后,user 确定存在
const { profile } = user;
const displayName = profile.settings.displayName.trim().toLowerCase();
// displayName 类型是 string,确定的
return <h1>{displayName}</h1>;
}
原则:在边界层(API 响应、props 传入)做一次空值检查,内部逻辑用确定类型。不要让 ?. 变成"我懒得想数据结构"的借口。
// ❌ 同一个项目里随机混用
interface UserProps { // 这里用interface
name: string;
}
type ButtonProps = { // 这里又用type
onClick: () => void;
}
interface ApiResponse { // 又interface
data: unknown;
}
type Theme = 'light' | 'dark'; // type
这不是语法错误,但没有一致性的代码让人读着累。
// ✅ 团队约定一个规则并统一执行
// 规则示例(不是唯一正确答案,关键是统一):
// type 用于:联合类型、交叉类型、工具类型、简单别名
type Status = 'active' | 'inactive';
type Nullable<T> = T | null;
type ButtonProps = { onClick: () => void; label: string };
// interface 用于:需要 extends 继承、第三方库声明合并
interface Repository {
findById(id: string): Promise<Entity>;
}
interface UserRepository extends Repository {
findByEmail(email: string): Promise<User>;
}
关键不是 interface 和 type 谁更好——而是你的项目有没有一个统一的规则。 没有规则 = 每次读代码都要猜"为什么这里用了 interface"。
// ❌ 简单场景用复杂泛型
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object
? T[P] extends Array<infer U>
? Array<DeepPartial<U>>
: DeepPartial<T[P]>
: T[P];
};
type ExtractRouteParams<T extends string> =
T extends `${infer _}:${infer Param}/${infer Rest}`
? { [K in Param]: string } & ExtractRouteParams<Rest>
: T extends `${infer _}:${infer Param}`
? { [K in Param]: string }
: {};
// 用这些类型的地方只有2处调用
能写出来说明你 TypeScript 水平很高。但:
// ✅ 问自己:这个泛型用了几次?
// 如果只用1-2次,直接写具体类型
// 替代 DeepPartial:手动写需要partial的字段
interface UpdateUserInput {
name?: string;
profile?: {
avatar?: string;
bio?: string;
};
}
// 替代复杂路由泛型:直接定义参数类型
interface RouteParams {
userId: string;
postId: string;
}
原则:类型是给人读的,不是给人秀的。如果一个泛型需要 3 行以上的条件类型,先问问有没有更简单的写法。
// ❌ tsconfig.json
{
"compilerOptions": {
"strict": false, // "先关了,以后再开"
// 或者更阴间的:
"strict": true,
"strictNullChecks": false, // 开了strict又关掉最重要的子选项
"noImplicitAny": false
}
}
strictNullChecks: false 意味着 TypeScript 认为所有值都不可能是 null 或 undefined。这等于关掉了 TypeScript 最有价值的安全检查之一。
// strictNullChecks: false 时,这段代码不报错
const user = users.find(u => u.id === id);
console.log(user.name); // user 可能是 undefined!运行时崩溃
// strictNullChecks: true 时,TypeScript 会逼你处理
const user = users.find(u => u.id === id);
if (!user) throw new Error(`User ${id} not found`);
console.log(user.name); // 安全
// ✅ 新项目直接开strict,老项目逐步开
{
"compilerOptions": {
"strict": true
// strict = 以下全部为true:
// strictNullChecks, noImplicitAny, strictFunctionTypes,
// strictBindCallApply, strictPropertyInitialization,
// noImplicitThis, alwaysStrict, useUnknownInCatchVariables
}
}
老项目怕一下全开报错太多? 用 // @ts-expect-error 逐个标记,然后建一个 TODO 列表慢慢修。比永远关着 strict 强一万倍。
| 反模式 | 修复 | 一句话 |
|---|---|---|
| any 当万能胶 | 定义具体类型 | 每个 any 都是定时炸弹 |
| catch 用 any | 用 unknown+ 类型守卫 | error 可能是任何东西 |
| as 断言满天飞 | 类型守卫/instanceof | as 是骗编译器 |
| enum 滥用 | union type + as const | 零运行时开销 |
| ?.可选链滥用 | 入口处一次空值检查 | 不要让 undefined 扩散 |
| interface/type 混用 | 团队统一规则 | 一致性比选择更重要 |
| 过度类型体操 | 用具体类型代替 | 类型是给人读的 |
| 关 strict | 开 strict 逐步修 | 最有价值的安全网 |
说实话,这 8 个我至少写过 5 个。特别是第 1 个和第 3 个——赶工期的时候 any 和 as 就是最快的"解决"方案。
但每次接手别人(或者三个月前的自己)充满 any 的代码时,就知道当初省的那 30 秒,现在要花 30 分钟来还。
你在 Code Review 中最常打回哪种写法?评论区聊聊。