跪拜 Guibai
← Back to the summary

The 8 TypeScript Anti-Patterns That Keep Surviving Code Review

Recently I code-reviewed code from three newcomers on the team and found the same problems cropping up repeatedly.

It's not logic errors — the TypeScript compiler catches those. It's the kind of writing that runs, but makes whoever inherits it want to punch something.

I've summarized the 8 most common anti-patterns. You're probably writing at least 3 of them right now.

Anti-pattern 1: any as a universal glue

// ❌ Slap `any` on whenever a type error appears
const handleResponse = (data: any) => {
  return data.result.items.map((item: any) => item.name);
};

Looks fine. But what if the structure of data changes? What if items doesn't exist? What if name becomes title?

TypeScript won't tell you — because you told it "I don't care about types."

// ✅ Spend 30 seconds defining the type
interface ApiResponse {
  result: {
    items: Array<{ name: string; id: number }>;
  };
}

const handleResponse = (data: ApiResponse) => {
  return data.result.items.map((item) => item.name);
};

Principle: For every extra any, your TypeScript degrades into JavaScript with type annotations.

If you genuinely don't know what the type is — use unknown. The next section explains why.

Anti-pattern 2: Using any instead of unknown in try-catch

// ❌ Using `any` in catch
 try {
  await fetchData();
} catch (error: any) {
  console.log(error.message);  // What if error isn't an Error object?
  console.log(error.response.status);  // What if there's no response?
}

The error in catch can be anything — not just an Error object. It could be a string, null, or even undefined.

// ✅ Use `unknown` + type guard
 try {
  await fetchData();
} catch (error: unknown) {
  if (error instanceof Error) {
    console.log(error.message);
  }
  if (isAxiosError(error)) {
    console.log(error.response?.status);
  }
}

unknown forces you to check the type before using it — any lets you pretend you know what it is.

Anti-pattern 3: Using as assertions instead of type guards

// ❌ Using `as` casts everywhere
const user = response.data as User;
const element = document.getElementById('root') as HTMLDivElement;
const config = JSON.parse(text) as AppConfig;

as means "I know better than the compiler." But do you really?

What if response.data doesn't return a User structure? What if that DOM element doesn't exist or isn't a div? Runtime crash, and TypeScript won't warn you.

// ✅ Use type guards for runtime checks
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)) {
  // Here `data` is narrowed to `User`, safe at both compile time and runtime
  console.log(data.name);
}

// For DOM elements, use `instanceof`
const element = document.getElementById('root');
if (element instanceof HTMLDivElement) {
  element.style.display = 'flex';
}

Principle: as lies to the compiler; type guards let the compiler verify for you.

The only reasonable scenario for as: you are 100% certain of the type, and the cost of adding a guard isn't worth it (e.g., mock data in test code).

Anti-pattern 4: Enum abuse (scenarios where a union type fits)

// ❌ Creating an enum for a few fixed values
enum Status {
  Active = 'active',
  Inactive = 'inactive',
  Pending = 'pending',
}

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

Enums look disciplined, but they have two problems:

  1. They generate extra runtime code after compilation (an IIFE object)
  2. Numeric enums are bidirectional mappings, which easily cause bugs
// ✅ Union type: zero runtime overhead, equally good type hints
type Status = 'active' | 'inactive' | 'pending';
type Direction = 'up' | 'down' | 'left' | 'right';

// Need to iterate over all values? Use a const array + typeof
const STATUSES = ['active', 'inactive', 'pending'] as const;
type Status = typeof STATUSES[number];

When to use an enum: When you need reverse mapping (number → name), or when the value needs to be used as an object (Status.Active) and the team has uniformly agreed to use enums. In other scenarios, union types are lighter.

Anti-pattern 5: Optional chaining ?. abuse leading to undefined hell

// ❌ Chaining `?.` all the way down, adding it to every property
const name = user?.profile?.settings?.displayName?.trim()?.toLowerCase();
// The type of `name` is `string | undefined`

const items = data?.response?.result?.items?.filter(i => i?.active);
// The type of `items` is `Item[] | undefined`

Optional chaining is great, but abusing it is equivalent to saying: "I'm not sure what this data structure looks like."

The result: Every variable can potentially be undefined, downstream code must add null checks everywhere, and undefined spreads like a contagion.

// ✅ Perform a single null check at the entry point, use definite types internally
function renderProfile(user: User | null) {
  if (!user) return <EmptyState />;
  
  // After the guard, `user` definitely exists
  const { profile } = user;
  const displayName = profile.settings.displayName.trim().toLowerCase();
  // The type of `displayName` is `string`, definite
  return <h1>{displayName}</h1>;
}

Principle: Perform a single null check at the boundary layer (API responses, incoming props), and use definite types for internal logic. Don't let ?. become an excuse for "I couldn't be bothered to think about the data structure."

Anti-pattern 6: Mixing interface and type without rules

// ❌ Randomly mixing within the same project
interface UserProps {  // `interface` used here
  name: string;
}

type ButtonProps = {  // `type` used here
  onClick: () => void;
}

interface ApiResponse {  // `interface` again
  data: unknown;
}

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

This isn't a syntax error, but code without consistency is tiring to read.

// ✅ The team agrees on a rule and enforces it uniformly
// Rule example (not the only correct answer, the key is consistency):

// `type` for: union types, intersection types, utility types, simple aliases
type Status = 'active' | 'inactive';
type Nullable<T> = T | null;
type ButtonProps = { onClick: () => void; label: string };

// `interface` for: scenarios needing `extends` inheritance, third-party library declaration merging
interface Repository {
  findById(id: string): Promise<Entity>;
}
interface UserRepository extends Repository {
  findByEmail(email: string): Promise<User>;
}

The key isn't whether interface or type is better — it's whether your project has a unified rule. No rule = guessing "why was interface used here" every time you read the code.

Anti-pattern 7: Excessive type gymnastics

// ❌ Complex generics for simple scenarios
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 }
      : {};

// These types are only used in 2 places

Being able to write this shows your TypeScript skills are high. But:

  1. Six months later, you won't understand it yourself
  2. Newcomers will just give up trying to understand
  3. IDE hints become an unreadable blob of expanded types
// ✅ Ask yourself: How many times is this generic used?
// If only 1-2 times, just write the concrete type directly

// Alternative to DeepPartial: manually write the fields that need to be partial
interface UpdateUserInput {
  name?: string;
  profile?: {
    avatar?: string;
    bio?: string;
  };
}

// Alternative to complex route generics: directly define the parameter type
interface RouteParams {
  userId: string;
  postId: string;
}

Principle: Types are for humans to read, not to show off. If a generic requires more than 3 lines of conditional types, first ask if there's a simpler way to write it.

Anti-pattern 8: Ignoring strict configuration

// ❌ tsconfig.json
{
  "compilerOptions": {
    "strict": false,  // "Turn it off for now, will enable later"
    // Or even more sinister:
    "strict": true,
    "strictNullChecks": false,  // Enabled strict but turned off the most important sub-option
    "noImplicitAny": false
  }
}

strictNullChecks: false means TypeScript assumes all values can never be null or undefined. This is equivalent to turning off one of TypeScript's most valuable safety checks.

// When strictNullChecks: false, this code doesn't error
const user = users.find(u => u.id === id);
console.log(user.name);  // `user` could be undefined! Runtime crash

// When strictNullChecks: true, TypeScript forces you to handle it
const user = users.find(u => u.id === id);
if (!user) throw new Error(`User ${id} not found`);
console.log(user.name);  // Safe
// ✅ Enable strict directly for new projects, gradually enable for old ones
{
  "compilerOptions": {
    "strict": true
    // strict = all of the following are true:
    // strictNullChecks, noImplicitAny, strictFunctionTypes,
    // strictBindCallApply, strictPropertyInitialization,
    // noImplicitThis, alwaysStrict, useUnknownInCatchVariables
  }
}

Afraid enabling everything at once on an old project will cause too many errors? Use // @ts-expect-error to mark them one by one, then create a TODO list and fix them gradually. Infinitely better than keeping strict off forever.

Quick Reference

Anti-pattern Fix One-liner
any as universal glue Define concrete types Every any is a ticking time bomb
any in catch Use unknown + type guard error can be anything
as assertions everywhere Type guards / instanceof as lies to the compiler
Enum abuse Union type + as const Zero runtime overhead
?. optional chaining abuse Single null check at entry point Don't let undefined spread
Mixing interface/type Unified team rule Consistency matters more than the choice
Excessive type gymnastics Use concrete types instead Types are for humans to read
Turning off strict Enable strict, fix gradually The most valuable safety net

How many have you written?

Honestly, I've written at least 5 of these 8. Especially the 1st and 3rd — when rushing a deadline, any and as are the fastest "solutions."

But every time I inherit someone else's (or my own from three months ago) code full of any, I know the 30 seconds saved back then now costs 30 minutes to repay.

What kind of code do you most often reject in Code Review? Let's chat in the comments.