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

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

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

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

反模式1:any 当万能胶

typescript 复制代码
// ❌ 遇到类型报错就any
const handleResponse = (data: any) => {
  return data.result.items.map((item: any) => item.name);
};

看着没问题。但 data 的结构变了呢?items 不存在了呢?name 改成 title 了呢?

TypeScript不会告诉你------因为你告诉它"我不在乎类型"。

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

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

catcherror 可能是任何东西------不只是 Error 对象。有可能是字符串、null、甚至 undefined。

typescript 复制代码
// ✅ 用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 断言代替类型守卫

typescript 复制代码
// ❌ 到处用 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 不会预警。

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 的场景)

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

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

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

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

typescript 复制代码
// ❌ 一路?.到底,每个属性都加
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 像传染病一样扩散。

typescript 复制代码
// ✅ 在入口处做一次空值检查,内部使用确定类型
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 混着用没规则

typescript 复制代码
// ❌ 同一个项目里随机混用
interface UserProps {  // 这里用interface
  name: string;
}

type ButtonProps = {  // 这里又用type
  onClick: () => void;
}

interface ApiResponse {  // 又interface
  data: unknown;
}

type Theme = 'light' | 'dark';  // type

这不是语法错误,但没有一致性的代码让人读着累

typescript 复制代码
// ✅ 团队约定一个规则并统一执行
// 规则示例(不是唯一正确答案,关键是统一):

// 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:过度类型体操

typescript 复制代码
// ❌ 简单场景用复杂泛型
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提示变成一坨不可读的展开类型
typescript 复制代码
// ✅ 问自己:这个泛型用了几次?
// 如果只用1-2次,直接写具体类型

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

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

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

反模式8:忽略 strict 配置

json 复制代码
// ❌ tsconfig.json
{
  "compilerOptions": {
    "strict": false,  // "先关了,以后再开"
    // 或者更阴间的:
    "strict": true,
    "strictNullChecks": false,  // 开了strict又关掉最重要的子选项
    "noImplicitAny": false
  }
}

strictNullChecks: false 意味着 TypeScript 认为所有值都不可能是 null 或 undefined。这等于关掉了 TypeScript 最有价值的安全检查之一。

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);  // 安全
json 复制代码
// ✅ 新项目直接开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 中最常打回哪种写法?评论区聊聊。

相关推荐
名字还没想好☜1 小时前
Next.js ‘use client‘ 到底加在哪:Server/Client Components 边界与常见报错
开发语言·前端·javascript·react·next.js
午安~婉1 小时前
GitHub Token/ GitHub Stats统计显示图异常
前端·github·vercel·github token
码云之上2 小时前
AI Agent 工程化总览篇:从 Prompt 到 Harness
前端·人工智能
2501_926978332 小时前
以说明书 DNA 为模板——完整 AGI 的结构图景
前端·人工智能·经验分享·笔记·ai写作
IT_陈寒2 小时前
Vite静态资源引用这个坑我踩得有点疼
前端·人工智能·后端
程序员黑豆3 小时前
鸿蒙应用开发:AttributeModifier 使用教程
前端·harmonyos
codeniu3 小时前
TRAE Work 实战 | 从"有个想法"到线上Demo,我全程只靠对话就完成了
前端·trae
妙码生花3 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十):增加管理员角色组管理
前端·后端·ai编程
飘尘3 小时前
一文讲清楚前端面试会问到的所有缓存
前端·javascript·面试