Claude Code for Type Safety: Advanced TypeScript, Branded Types, and Runtime Validation — Claude Skills 360 Blog
Blog / Development / Claude Code for Type Safety: Advanced TypeScript, Branded Types, and Runtime Validation
Development

Claude Code for Type Safety: Advanced TypeScript, Branded Types, and Runtime Validation

Published: August 8, 2026
Read time: 9 min read
By: Claude Skills 360

TypeScript types disappear at runtime — the type system can’t catch a bug where you pass a userId where a productId is expected (both are string). Advanced TypeScript patterns fix this: branded types make the compiler reject wrong IDs, Zod schemas validate runtime data and infer TypeScript types, and discriminated unions make impossible states unrepresentable. Claude Code generates these patterns correctly.

Branded Types

We're passing userId where productId is expected and TypeScript doesn't catch it.
Both are UUIDs typed as string. Use branded types to make this a compile error.
// types/branded.ts
declare const __brand: unique symbol;
type Brand<T, B extends string> = T & { readonly [__brand]: B };

// ID types — assignment is restricted by brand
export type UserId = Brand<string, 'UserId'>;
export type ProductId = Brand<string, 'ProductId'>;
export type OrderId = Brand<string, 'OrderId'>;

// Constructor functions — coerce at system boundaries only
export const UserId = (id: string): UserId => {
  if (!/^[0-9a-f-]{36}$/.test(id)) throw new Error(`Invalid UserId: ${id}`);
  return id as UserId;
};

export const ProductId = (id: string): ProductId => {
  if (!/^[0-9a-f-]{36}$/.test(id)) throw new Error(`Invalid ProductId: ${id}`);
  return id as ProductId;
};

// Now the compiler catches mixed IDs
function getOrdersForUser(userId: UserId) {
  return db.orders.where({ userId });
}

const productId = ProductId('abc-123-...');
getOrdersForUser(productId); // ✗ Compile error: ProductId is not assignable to UserId

const userId = UserId('xyz-789-...');
getOrdersForUser(userId);    // ✓ Compiles
// types/units.ts — brand primitive values with units
type Cents = Brand<number, 'Cents'>;
type Percentage = Brand<number, 'Percentage'>;
type Milliseconds = Brand<number, 'Milliseconds'>;

const Cents = (n: number): Cents => {
  if (!Number.isInteger(n) || n < 0) throw new Error(`Invalid Cents: ${n}`);
  return n as Cents;
};

function addTax(priceCents: Cents, taxRate: Percentage): Cents {
  return Cents(Math.round(priceCents * (1 + taxRate / 100)));
}

const price = Cents(999);
const tax = 8.5 as Percentage;
addTax(price, tax);      // ✓
addTax(999, tax);        // ✗ Cannot pass raw number as Cents
addTax(price, 999);      // ✗ Cannot pass Cents as Percentage

Discriminated Unions

Model our order state machine in TypeScript.
Different states have different data. Make impossible states unrepresentable.
// types/order.ts
// Each state carries only the data that makes sense for that state
type Order =
  | {
      status: 'draft';
      id: OrderId;
      customerId: UserId;
      items: OrderItem[];
      // No payment, shipping — doesn't exist yet
    }
  | {
      status: 'placed';
      id: OrderId;
      customerId: UserId;
      items: OrderItem[];
      placedAt: Date;
      // Still no payment
    }
  | {
      status: 'paid';
      id: OrderId;
      customerId: UserId;
      items: OrderItem[];
      placedAt: Date;
      paymentId: string;
      paidAt: Date;
      // Still no shipping
    }
  | {
      status: 'shipped';
      id: OrderId;
      customerId: UserId;
      items: OrderItem[];
      placedAt: Date;
      paymentId: string;
      paidAt: Date;
      trackingNumber: string;
      shippedAt: Date;
    }
  | {
      status: 'cancelled';
      id: OrderId;
      customerId: UserId;
      items: OrderItem[];
      placedAt: Date;
      cancelledAt: Date;
      refundId?: string; // Only if payment was made
    };

// Exhaustive switch — TypeScript errors if you miss a case
function getStatusLabel(order: Order): string {
  switch (order.status) {
    case 'draft': return 'Draft';
    case 'placed': return 'Awaiting payment';
    case 'paid': return `Paid on ${order.paidAt.toLocaleDateString()}`;
    case 'shipped': return `Shipped — tracking: ${order.trackingNumber}`;
    case 'cancelled': return 'Cancelled';
    // Add a new status and the switch is incomplete → compile error
    default: {
      const _exhaustive: never = order; // Type error if any case missed
      return 'Unknown';
    }
  }
}

// Type narrowing — no null checks needed
function getTrackingNumber(order: Order): string | null {
  if (order.status === 'shipped') {
    return order.trackingNumber; // TypeScript knows this exists for 'shipped'
  }
  return null;
}

Zod for Runtime Validation + TypeScript

Our API receives user input that we want both validated at runtime
and type-safe in TypeScript. Avoid defining types and validators separately.
// schemas/user.ts
import { z } from 'zod';

// Single source of truth: Zod schema defines both validation AND types
export const CreateUserSchema = z.object({
  email: z.string().email('Invalid email format').toLowerCase(),
  name: z.string().min(2, 'Name must be at least 2 characters').max(100).trim(),
  age: z.number().int().min(13, 'Must be 13 or older').max(150).optional(),
  role: z.enum(['user', 'moderator', 'admin']).default('user'),
  metadata: z.record(z.string(), z.unknown()).optional(),
});

// TypeScript type inferred from schema — no duplication
export type CreateUserInput = z.infer<typeof CreateUserSchema>;
// Equivalent to: { email: string; name: string; age?: number; role: 'user' | 'moderator' | 'admin'; metadata?: Record<string, unknown> }

// Strict vs lenient parsing
export function parseCreateUser(input: unknown): CreateUserInput {
  return CreateUserSchema.parse(input); // Throws ZodError with detailed messages on failure
}

export function safeParseCreateUser(input: unknown): z.SafeParseReturnType<CreateUserInput, CreateUserInput> {
  return CreateUserSchema.safeParse(input); // Returns { success: true, data } or { success: false, error }
}
// In Express handler
app.post('/api/users', async (req, res) => {
  const result = safeParseCreateUser(req.body);

  if (!result.success) {
    // Format Zod errors for API response
    const errors = result.error.flatten().fieldErrors;
    return res.status(400).json({ error: 'Validation failed', fields: errors });
  }

  // result.data is fully typed as CreateUserInput
  const user = await createUser(result.data);
  res.json(user);
});

Advanced Type Utilities

// types/utils.ts — useful type utilities

// Make some required fields optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

type UpdateUserInput = PartialBy<CreateUserInput, 'role' | 'age'>;
// email and name still required, role and age optional

// Extract only keys with a specific value type
type KeysOfType<T, V> = { [K in keyof T]: T[K] extends V ? K : never }[keyof T];

type StringKeys = KeysOfType<User, string>; // 'id' | 'email' | 'name'
type NumberKeys = KeysOfType<User, number>; // 'age' | 'loginCount' | ...

// Type-safe event emitter
type EventMap = {
  'user:created': { userId: UserId; email: string };
  'order:shipped': { orderId: OrderId; trackingNumber: string };
  'payment:failed': { orderId: OrderId; reason: string };
};

class TypedEventEmitter {
  private handlers: Partial<{
    [K in keyof EventMap]: Array<(data: EventMap[K]) => void>
  }> = {};

  on<E extends keyof EventMap>(event: E, handler: (data: EventMap[E]) => void) {
    (this.handlers[event] ??= []).push(handler as any);
    return this;
  }

  emit<E extends keyof EventMap>(event: E, data: EventMap[E]) {
    this.handlers[event]?.forEach(h => (h as any)(data));
  }
}

// Fully typed usage — arguments are validated by TypeScript
const emitter = new TypedEventEmitter();

emitter.on('order:shipped', ({ orderId, trackingNumber }) => {
  // orderId is OrderId, trackingNumber is string — no type assertions needed
  sendShippingNotification(orderId, trackingNumber);
});

emitter.emit('order:shipped', {
  orderId: OrderId('...'),
  trackingNumber: '1Z999AA10123456784',
});
// Emitting wrong event type: TypeScript error

Template Literal Types

// Type-safe API routes
type RouteParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof RouteParams<Rest>]: string }
    : T extends `${string}:${infer Param}`
      ? { [K in Param]: string }
      : Record<never, never>;

type UserRouteParams = RouteParams<'/users/:userId/orders/:orderId'>;
// Inferred as: { userId: string; orderId: string }

// Type-safe event names
type EventName = `${string}:${'created' | 'updated' | 'deleted'}`;
const event: EventName = 'user:created';   // ✓
const bad: EventName = 'user:fetched';     // ✗ 'fetched' not in union

For applying these TypeScript patterns to large-scale refactoring, see the TypeScript guide. For Zod in API validation specifically, see the API design guide. The Claude Skills 360 bundle includes type safety skill sets for branded types, discriminated unions, and Zod integration. Start with the free tier to try type system hardening.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free