< sooo.dev />

Those TypeScript Patterns You're Using? Yeah, They're Wrong

You've been told TypeScript makes your code safer, but you're probably using it to create even more elaborate ways to shoot yourself in the foot. Here's why your favorite TypeScript patterns are actually making things worse.

Share:
Those TypeScript Patterns You're Using? Yeah, They're Wrong

Those TypeScript Patterns You’re Using? Yeah, They’re Wrong

So you’ve finally embraced TypeScript. Congratulations! You’ve graduated from “hoping for the best” to “creating elaborate type systems that take longer to write than the actual code.” Progress!

But here’s the uncomfortable truth: most of your TypeScript code is probably making your codebase worse, not better. You’re not using a safety harness; you’re wearing a costume of one while juggling chainsaws.

The Five Stages of TypeScript Grief

  1. Denial - “JavaScript is fine! I don’t need types.”
  2. Anger - “Why does this stupid compiler keep complaining?!”
  3. Bargaining - “If I just add any here, the errors will go away…”
  4. Depression - “I have 427 type errors and no idea how to fix them.”
  5. Acceptance - “Oh, I see. I’ve been thinking about types all wrong.”

Most developers are stuck somewhere between stages 2 and 3, angrily sprinkling any and as any throughout their codebase like a toddler with a salt shaker.

Pattern #1: Type Assertion Addiction

// What you're doing
const userData = JSON.parse(response) as UserData;

// What you should be doing
const userData: UserData = JSON.parse(response);
if (!isUserData(userData)) {
  throw new Error('Invalid user data received');
}

Type assertions (as Something) are basically you telling TypeScript “trust me bro, I know what I’m doing” – the programming equivalent of holding your beer before doing something stupid.

Every time you use as, you’re creating a potential runtime error that TypeScript can’t protect you from. You’re literally telling the compiler to ignore its own safety checks. That’s like disabling your car’s airbags because the warning light was annoying you.

Pattern #2: The any Escape Hatch

// The path to TypeScript hell
function processData(data: any) {
  return data.map(item => item.value);
}

Using any in TypeScript is like installing a security system in your house but leaving a sign outside that says “KEY UNDER THE DOORMAT.” It completely defeats the purpose.

Each any in your codebase represents a tiny black hole where type safety goes to die. And like actual black holes, they tend to spread, corrupting everything they touch until your once-typed codebase is just JavaScript with extra steps.

Pattern #3: Overly Generic Generics

// Exhibit A: Generic overkill
type FetchResult<T, E extends Error = Error, M extends Metadata = Metadata> = 
  | { status: 'success'; data: T; metadata: M }
  | { status: 'error'; error: E; metadata: M };

Your generics don’t make you look smart; they make your codebase unmaintainable. If your type definition has more generic parameters than a pharmaceutical company has lawyers, you’ve probably gone too far.

Remember: the goal is to make your code more understandable, not to create type puzzles that make seasoned developers question their career choices.

Pattern #4: Interface Inheritance Hierarchies From Hell

// The corporate org chart of types
interface Entity { id: string; }
interface NamedEntity extends Entity { name: string; }
interface TimestampedEntity extends Entity { createdAt: Date; updatedAt: Date; }
interface NamedTimestampedEntity extends NamedEntity, TimestampedEntity {}
interface User extends NamedTimestampedEntity { email: string; }
interface AdminUser extends User { permissions: string[]; }

Every time you create an inheritance chain more than two levels deep, a maintainability angel loses its wings. Your types should not look like the org chart of a multinational corporation.

Composition > inheritance applies to your types too. Your future self (or the poor developer who inherits your code) will thank you for keeping things flat and explicit.

Pattern #5: String Literal Type Tetris

// Type Tetris Championship entry
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type ResourceType = 'user' | 'post' | 'comment';
type CacheStrategy = 'no-cache' | 'cache-first' | 'network-first';
type ApiEndpoint = `/${ResourceType}s/${string}`;
type RequestConfig = {
  method: HttpMethod;
  endpoint: ApiEndpoint;
  cacheStrategy: CacheStrategy;
};

Template literal types and string unions are powerful, but with great power comes great responsibility… which you’re probably ignoring as you create ever more elaborate string type combinations.

The more complex your string type puzzles become, the harder it is for humans (including you) to understand what values are actually allowed. At some point, you’re just writing regular expressions with extra steps.

The Sins of Modern TypeScript

Destructuring Type Declarations

// What fresh hell is this?
const { id, name, email }: { id: string; name: string; email: string } = user;

Just because you can inline your types anywhere doesn’t mean you should. This is like hiding Easter eggs in your code, except instead of chocolate, they’re landmines of complexity.

Create a proper type or interface at the top level and use that. Your coworkers shouldn’t need to play “Where’s Waldo?” with your type definitions.

Type Assertion Chains

// The TypeScript centipede
const userId = ((response as ApiResponse).data as UserResponseData).user as User).id as string;

If your type assertions need their own line break, you’re doing something very, very wrong. This is the TypeScript equivalent of trying to fix your plumbing with duct tape – it might hold temporarily, but catastrophic failure is inevitable.

Boolean Return Type Functions

// The function that lies
function isValidUser(user: unknown): boolean {
  // 50 lines of type checking...
  return true;
}

// What you should be doing
function isValidUser(user: unknown): user is User {
  // Same 50 lines, but now with actual type safety
  return true;
}

Type predicates (user is User) exist for a reason. When you return a simple boolean from validation functions, you’re forcing TypeScript to forget all the type narrowing work you just did. It’s like doing a security check at the door but not giving approved people a wristband.

The TypeScript Reform Program

Here’s how to fix your broken TypeScript patterns:

  1. Use type guards, not assertions - Validate your data at runtime instead of lying to the compiler.

  2. Create runtime validation - Types disappear at runtime, so make sure your validation doesn’t. Consider libraries like Zod, io-ts, or runtypes.

  3. Keep type definitions simple - If your type definition needs its own documentation, it’s probably too complex.

  4. Embrace branded types for nominal typing - When you need to distinguish between strings, numbers, or objects that are structurally the same but semantically different.

// Branded types example
type UserId = string & { readonly __brand: unique symbol };

function createUserId(id: string): UserId {
  return id as UserId;
}

// Now you can't accidentally use a random string as a UserId
  1. Write explicit return types - Don’t rely on inference for public API functions. Be explicit about what your functions return.

  2. Use discriminated unions - They’re the best way to handle different states or types.

// Discriminated union example
type State = 
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: User[] }
  | { status: 'error'; error: Error };

The Promised Land: TypeScript That Actually Helps

The goal isn’t to impress other developers with your TypeScript wizardry. The goal is to write code that:

  1. Catches errors at compile time
  2. Makes refactoring safer
  3. Serves as documentation
  4. Is understandable by humans, not just compilers

Your types should be a tool for clarity, not a weapon of confusion.

Conclusion: TypeScript Redemption Arc

TypeScript, like any powerful tool, can be used for good or evil. Right now, there’s a lot of evil out there masquerading as “advanced TypeScript patterns.”

The true masters aren’t the ones writing the most complex types; they’re the ones writing the simplest types that still catch all the errors.

So take a long, hard look at your TypeScript code. Is it actually making your codebase safer and more maintainable? Or is it just JavaScript wearing an elaborate disguise?

Remember: The best TypeScript code is the code that looks like it could almost be plain JavaScript, but with just enough type annotations to catch the real errors. Anything more complex is probably technical debt in disguise.

Now go forth and refactor, before your tech debt catches up with you.

Photo of Emma Stylesheet

About Emma Stylesheet

The CSS purist who judges your design choices with the severity of a 90s web design professor. Emma has strong opinions about semantic HTML, responsive approaches, and will fight you about cascade layers. She's equal parts excited and terrified by how CSS is evolving, and can spot a z-index issue with her eyes closed. Every design crime you commit ends up in her 'responsive failures' collection.