TypeScript Tips for Cleaner Code

Five patterns I reach for regularly to keep TypeScript codebases readable, refactor-friendly, and free of the defensive boilerplate that accumulates over time.

  • typescript
  • dx

TypeScript is a fantastic tool, but it’s easy to end up with code that’s technically typed but hard to read — overloaded with as, !, and redundant generics. Here are five patterns I’ve found genuinely useful for keeping things clean.

1. Prefer satisfies Over Type Assertions

as silences the type checker. satisfies validates without widening:

const config = {
  port: 3000,
  host: 'localhost',
} satisfies Record<string, string | number>;

// config.port is still narrowed to number, not string | number

Use satisfies when you want to validate the shape of a literal without losing the inferred type.

2. Discriminated Unions Over Optional Fields

Optional fields are easy to add but hard to reason about. Discriminated unions make illegal states unrepresentable:

// Avoid
type Result = { ok: boolean; data?: User; error?: string };

// Prefer
type Result =
  | { ok: true; data: User }
  | { ok: false; error: string };

The exhaustive check in a switch on result.ok then catches missing cases at compile time.

3. const Assertions for Configuration Objects

When you define a lookup table or options array, as const narrows values to their literal types and makes the object readonly — useful for satisfies pairs and for driving union types:

const ROLES = ['admin', 'editor', 'viewer'] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"

4. Template Literal Types for String Contracts

Template literal types let you enforce string formats in the type system without runtime validation:

type EventName = `on${Capitalize<string>}`;

function on(event: EventName, handler: () => void) { /* ... */ }

on('onClick', () => {}); // ok
on('click', () => {});   // error: must start with "on"

They’re also useful for building typed API route strings or CSS property names.

5. Use unknown Instead of any at Boundaries

any disables type checking entirely. unknown forces you to narrow before use, which is exactly what you want at system boundaries like API responses or JSON parsing:

async function fetchUser(id: string): Promise<unknown> {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

const data = await fetchUser('123');
// Must narrow before using:
if (isUser(data)) { /* data is User here */ }

A Zod schema is the cleanest way to both validate and narrow at once — parse, don’t validate.


None of these are groundbreaking, but they compound. A codebase that uses discriminated unions, satisfies, and unknown at its edges tends to stay maintainable as it grows because the type system carries more of the reasoning load.