Fullstack/TypeScript Patterns

TypeScript Patterns

Patterns I use consistently across projects. Nothing fancy, just things that save time and reduce bugs.


Interfaces vs Types

Use interface for object shapes. Use type for unions, intersections, and primitives.

// Object shape → interface
interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}

// Union → type
type Status = "active" | "inactive" | "pending";

// Intersection → type
type AdminUser = User & { role: "admin"; permissions: string[] };

Utility Types

// Partial makes all fields optional
type UserUpdate = Partial<User>;

// Pick selects specific fields
type UserPreview = Pick<User, "id" | "name">;

// Omit removes specific fields
type UserWithoutDates = Omit<User, "createdAt" | "updatedAt">;

// Record for key-value maps
type StatusMap = Record<string, Status>;

// ReturnType extracts function return type
type ApiResponse = ReturnType<typeof fetchUser>;

Generic Patterns

// Generic API response wrapper
interface ApiResult<T> {
  data: T;
  error: string | null;
  status: number;
}

// Generic with constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

// Generic React component
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

Discriminated Unions

Useful for state machines and API responses.

type LoadingState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: User[] }
  | { status: "error"; message: string };

function render(state: LoadingState) {
  switch (state.status) {
    case "idle": return <p>Not started</p>;
    case "loading": return <Spinner />;
    case "success": return <UserList users={state.data} />;
    case "error": return <p>Error: {state.message}</p>;
  }
}

Zod for Runtime Validation

import { z } from "zod";

const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
});

type User = z.infer<typeof UserSchema>;

// Validate API input
const result = UserSchema.safeParse(req.body);
if (!result.success) {
  return Response.json({ error: result.error.issues }, { status: 400 });
}

Last updated: September 2026.

Last updated · September 2026