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

193577746(kyriewen) · August 04, 2026 · 14 hits

最近 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 中最常打回哪种写法?评论区聊聊。

No Reply at the moment.
You need to Sign in before reply, if you don't have an account, please Sign up first.