聊天讨论 别再这样写 TypeScript 了——Code Review 中最常见的 8 个反模式

193577746(kyriewen) · 2026年08月04日 · 14 次阅读

最近 Code Review 了组里三个新人的代码,发现同样的问题反复出现。

不是逻辑错误——TypeScript 编译器会帮你抓。是那种能跑,但让接手的人想打人的写法。

总结了 8 个最常见的反模式。你可能正在写其中至少 3 个。

反模式 1:any 当万能胶

// ❌ 遇到类型报错就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,下一节说为什么。

反模式 2:try-catch 里用 any 而不是 unknown

// ❌ catch里用any
try {
  await fetchData();
} catch (error: any) {
  console.log(error.message);  // 如果error不是Error对象呢?
  console.log(error.response.status);  // 如果没有response呢?
}

catcherror 可能是任何东西——不只是 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 则让你假装知道它是什么。

反模式 3:as 断言代替类型守卫

// ❌ 到处用 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 数据)。

反模式 4:枚举滥用(该用 union type 的场景)

// ❌ 为了几个固定值搞个enum
enum Status {
  Active = 'active',
  Inactive = 'inactive',
  Pending = 'pending',
}

enum Direction {
  Up = 'up',
  Down = 'down',
  Left = 'left',
  Right = 'right',
}

enum 看着很规范,但它有两个问题:

  1. 编译后会生成额外的运行时代码(一个 IIFE 对象)
  2. 数字枚举是双向映射,容易出 bug
// ✅ 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 更轻量。

反模式 5:可选链?.滥用导致 undefined 地狱

// ❌ 一路?.到底,每个属性都加
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 传入)做一次空值检查,内部逻辑用确定类型。不要让 ?. 变成"我懒得想数据结构"的借口。

反模式 6:interface 和 type 混着用没规则

// ❌ 同一个项目里随机混用
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"。

反模式 7:过度类型体操

// ❌ 简单场景用复杂泛型
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. 新人看到直接放弃理解
  3. IDE 提示变成一坨不可读的展开类型
// ✅ 问自己:这个泛型用了几次?
// 如果只用1-2次,直接写具体类型

// 替代 DeepPartial:手动写需要partial的字段
interface UpdateUserInput {
  name?: string;
  profile?: {
    avatar?: string;
    bio?: string;
  };
}

// 替代复杂路由泛型:直接定义参数类型
interface RouteParams {
  userId: string;
  postId: string;
}

原则:类型是给人读的,不是给人秀的。如果一个泛型需要 3 行以上的条件类型,先问问有没有更简单的写法。

反模式 8:忽略 strict 配置

//  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 个——赶工期的时候 anyas 就是最快的"解决"方案。

但每次接手别人(或者三个月前的自己)充满 any 的代码时,就知道当初省的那 30 秒,现在要花 30 分钟来还。

你在 Code Review 中最常打回哪种写法?评论区聊聊。

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请 注册新账号