^.^;
Back to Blog
Blog Post

TypeScript Patterns That Actually Make Code Better

Beyond basic types - discriminated unions, branded types, and advanced patterns that prevent bugs before they happen. Real examples from production code.

J
JG
Author
2025-11-15
Published
◆ ◆ ◆

TypeScript Patterns That Actually Make Code Better

TypeScript is more than just "JavaScript with types". When used properly, it can prevent entire classes of bugs before you even run your code.

Here are the patterns I use every day that have saved me countless hours of debugging.

1. Discriminated Unions (Tagged Unions)

This is the most powerful pattern in TypeScript.

The Problem

interface SuccessResponse {
  data: User[];
}

interface ErrorResponse {
  error: string;
}

type ApiResponse = SuccessResponse | ErrorResponse;

function handleResponse(response: ApiResponse) {
  // ❌ TypeScript doesn't know which one it is
  console.log(response.data); // Error: Property 'data' might not exist
}

The Solution

interface SuccessResponse {
  status: 'success'; // Discriminant property
  data: User[];
}

interface ErrorResponse {
  status: 'error'; // Discriminant property
  error: string;
}

type ApiResponse = SuccessResponse | ErrorResponse;

function handleResponse(response: ApiResponse) {
  if (response.status === 'success') {
    // ✅ TypeScript knows it's SuccessResponse
    console.log(response.data); // No error!
  } else {
    // ✅ TypeScript knows it's ErrorResponse
    console.log(response.error); // No error!
  }
}

Real-world usage in my labs:

type AttackStage =
  | { stage: 'idle' }
  | { stage: 'scanning'; progress: number }
  | { stage: 'exploiting'; target: string; progress: number }
  | { stage: 'complete'; results: ExploitResult[] };

function renderAttack(state: AttackStage) {
  switch (state.stage) {
    case 'idle':
      return <IdleState />;
    case 'scanning':
      return <ScanningState progress={state.progress} />;
    case 'exploiting':
      return <ExploitingState target={state.target} progress={state.progress} />;
    case 'complete':
      return <CompleteState results={state.results} />;
  }
}

No runtime checks needed - TypeScript guarantees the correct properties exist.

2. Branded Types (Nominal Types)

Prevent mixing up similar primitive types.

The Problem

type UserId = string;
type Email = string;

function sendEmail(email: Email) {
  // Send email
}

const userId: UserId = "user123";
sendEmail(userId); // ❌ Compiles fine but is wrong!

The Solution

type UserId = string & { readonly __brand: 'UserId' };
type Email = string & { readonly __brand: 'Email' };

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

function createEmail(email: string): Email {
  if (!email.includes('@')) throw new Error('Invalid email');
  return email as Email;
}

function sendEmail(email: Email) {
  // Send email
}

const userId = createUserId("user123");
const email = createEmail("user@example.com");

sendEmail(userId); // ✅ TypeScript error!
sendEmail(email);  // ✅ Works!

Real-world usage:

type IPv4Address = string & { readonly __brand: 'IPv4' };
type MACAddress = string & { readonly __brand: 'MAC' };
type Hash = string & { readonly __brand: 'Hash' };

function validateIPv4(ip: string): IPv4Address {
  const regex = /^(\d{1,3}\.){3}\d{1,3}$/;
  if (!regex.test(ip)) throw new Error('Invalid IPv4');
  return ip as IPv4Address;
}

3. Template Literal Types

Type-safe string manipulation.

type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Route = '/users' | '/posts' | '/comments';
type Endpoint = `${HTTPMethod} ${Route}`;

// Valid:
const endpoint1: Endpoint = 'GET /users';
const endpoint2: Endpoint = 'POST /comments';

// Invalid:
const endpoint3: Endpoint = 'PATCH /users'; // ✅ Error!
const endpoint4: Endpoint = 'GET /invalid'; // ✅ Error!

Real-world usage in my labs:

type LabId = 'red-team' | 'blue-team' | 'purple-team';
type ModuleName = 'nmap' | 'hydra' | 'burpsuite';
type ModulePath = `/${LabId}-lab/${ModuleName}`;

// Generates all valid paths:
// '/red-team-lab/nmap'
// '/blue-team-lab/hydra'
// etc.

4. Const Assertions

Make objects deeply readonly and infer literal types.

Without const assertion:

const config = {
  api: 'https://api.example.com',
  timeout: 5000
};

// Type: { api: string; timeout: number; }

With const assertion:

const config = {
  api: 'https://api.example.com',
  timeout: 5000
} as const;

// Type: { readonly api: "https://api.example.com"; readonly timeout: 5000; }

Real-world usage:

const ATTACK_PHASES = [
  'reconnaissance',
  'weaponization',
  'delivery',
  'exploitation',
  'installation',
  'command-and-control',
  'actions-on-objectives'
] as const;

type AttackPhase = typeof ATTACK_PHASES[number];
// Type: "reconnaissance" | "weaponization" | ... (exact literals!)

function setPhase(phase: AttackPhase) {
  // Only accepts exact phase names
}

setPhase('reconnaissance'); // ✅
setPhase('invalid'); // ✅ Error!

5. Utility Types for State Management

Zustand store with proper typing:

interface CybersecurityStore {
  moduleProgress: Record<string, ModuleProgress>;
  markModuleComplete: (moduleId: string) => void;
  resetProgress: () => void;
}

// Extract just the state
type State = Omit<CybersecurityStore, keyof Actions>;

// Extract just the actions
interface Actions {
  markModuleComplete: CybersecurityStore['markModuleComplete'];
  resetProgress: CybersecurityStore['resetProgress'];
}

6. Exhaustiveness Checking

Ensure you handle all cases:

type AlertLevel = 'low' | 'medium' | 'high' | 'critical';

function getAlertColor(level: AlertLevel): string {
  switch (level) {
    case 'low':
      return 'green';
    case 'medium':
      return 'yellow';
    case 'high':
      return 'orange';
    case 'critical':
      return 'red';
    default:
      // ✅ This ensures all cases are handled
      const exhaustiveCheck: never = level;
      throw new Error(`Unhandled level: ${exhaustiveCheck}`);
  }
}

// If you add a new level, TypeScript will error until you handle it!

7. Conditional Types

Types that depend on other types:

type IsArray<T> = T extends any[] ? true : false;

type A = IsArray<string[]>; // true
type B = IsArray<string>;   // false

Real-world usage:

type ApiResponse<T> = T extends void
  ? { status: 'success' }
  : { status: 'success'; data: T } | { status: 'error'; error: string };

// If T is void, only status field exists
const voidResponse: ApiResponse<void> = { status: 'success' };

// Otherwise, data or error exists
const dataResponse: ApiResponse<User[]> = {
  status: 'success',
  data: []
};

8. Type Guards

Runtime type checking with type narrowing:

interface NetworkPacket {
  type: 'tcp' | 'udp';
  source: string;
  destination: string;
}

interface TCPPacket extends NetworkPacket {
  type: 'tcp';
  flags: string[];
}

interface UDPPacket extends NetworkPacket {
  type: 'udp';
  length: number;
}

function isTCPPacket(packet: NetworkPacket): packet is TCPPacket {
  return packet.type === 'tcp';
}

function handlePacket(packet: NetworkPacket) {
  if (isTCPPacket(packet)) {
    // ✅ TypeScript knows it's TCPPacket
    console.log(packet.flags);
  }
}

9. Mapped Types

Transform existing types:

type Optional<T> = {
  [K in keyof T]?: T[K];
};

type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

interface User {
  id: string;
  name: string;
  email: string;
}

type PartialUser = Optional<User>;
// { id?: string; name?: string; email?: string; }

type ReadonlyUser = Readonly<User>;
// { readonly id: string; readonly name: string; readonly email: string; }

10. infer Keyword

Extract types from complex types:

type GetArrayType<T> = T extends (infer U)[] ? U : never;

type StringArray = GetArrayType<string[]>; // string
type NumberArray = GetArrayType<number[]>; // number

Real-world usage:

type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getUser() {
  return { id: '1', name: 'John' };
}

type UserType = GetReturnType<typeof getUser>;
// { id: string; name: string; }

Conclusion

These patterns have dramatically reduced bugs in my code:

  • No more `undefined is not a function`
  • No more accessing properties that don't exist
  • No more mixing up similar string types
  • Compile-time verification of business logic

TypeScript isn't just about adding types - it's about modeling your domain accurately and letting the compiler work for you.


Using TypeScript effectively? I'd love to hear about your patterns!


James G. - AI Alchemist | Full-Stack Developer

END OF ARTICLE
J

About JG

Full-stack developer specializing in web performance, authentication systems, and developer experience. Passionate about sharing real-world debugging stories and optimization techniques.

Terms of ServiceLicense AgreementPrivacy Policy
Copyright © 2025 JMFG. All rights reserved.