DEV Community

Cover image for 7 Advanced TypeScript Patterns for Safer, Smarter Code Design
Tony St Pierre
Tony St Pierre

Posted on

7 Advanced TypeScript Patterns for Safer, Smarter Code Design

Most types describe.
The best types decide.

In this post, I share 7 patterns that turn TypeScript into a design tool, not just a safety net. Branded types, state machines, proof-carrying params, and more.

Write types that teach.
Types that prevent.
Types that leave nothing to chance.

Read it here: Designing with Types

Top comments (2)

Collapse
 
kris_chou_5f6deb607e8cb75 profile image
Kris Chou

Thanks, @tonystpierre, this is really helpful and insightful.
Please keep sharing content like this!

Collapse
 
jayswebdev83 profile image
Jay
  1. Nominal Typing via Branding By default, TypeScript uses structural typing, but you can simulate nominal types (like in Rust or Haskell) to prevent misuse of similar-looking values.

ts
Copy
Edit
type USD = number & { readonly __brand: "USD" };
type EUR = number & { readonly __brand: "EUR" };

function toUSD(amount: number): USD {
return amount as USD;
}

function chargeInUSD(amount: USD) {
// ...
}
✅ Why it matters: Encourages precise domain modeling. Helps catch bugs early in finance, units, and IDs.

  1. Discriminated Unions for State Machines Model state transitions and enforce exhaustive handling with discriminated unions.

ts
Copy
Edit
type State =
| { status: "loading" }
| { status: "error"; message: string }
| { status: "success"; data: string };

function handle(state: State) {
switch (state.status) {
case "loading":
case "error":
case "success":
break;
// If we forget a case, TypeScript can warn us (with never trick).
}
}
✅ Why it matters: Models finite states clearly. Eliminates impossible states. Encourages robust state handling.

  1. Literal Types to Encode Options Use string literals or enums to model allowed options precisely.

ts
Copy
Edit
type ButtonSize = "small" | "medium" | "large";

function createButton(size: ButtonSize) {
// TS enforces valid values
}
✅ Why it matters: Improves intent. Reduces bugs from invalid parameters. Replaces fragile string or any.

  1. Type-Level Validation and Computation Leverage conditional types and mapped types to validate or derive new types.

ts
Copy
Edit
type Without = Pick>;

type User = { id: number; name: string; password: string };
type PublicUser = Without;
✅ Why it matters: Enables reusable logic at the type level. Prevents leaking sensitive fields (like passwords).

  1. Fluent Builders with Type Constraints Create builder patterns that guide usage step by step, enforcing valid sequences.

ts
Copy
Edit
class FormBuilder {
private fields: string[] = [];

addField(name: string): this {
this.fields.push(name);
return this;
}

build(): Form {
return { fields: this.fields };
}
}
✅ Why it matters: Guides API usage. Prevents partial or invalid object construction.

  1. Function Overloads for UX and Type Inference Provide overloaded signatures for a function to guide different valid call shapes.

ts
Copy
Edit
function getValue(key: "user"): string;
function getValue(key: "id"): number;
function getValue(key: string): string | number {
return key === "id" ? 123 : "Alice";
}
✅ Why it matters: Improves dev experience. Offers intelligent autocomplete and intent clarification.

  1. Domain-Specific Type Guards Write smart type guards that refine types, acting like runtime enforcement.

ts
Copy
Edit
type Animal = { kind: "cat"; meow(): void } | { kind: "dog"; bark(): void };

function isCat(animal: Animal): animal is Extract {
return animal.kind === "cat";
}
✅ Why it matters: Enables polymorphism. Refines types intelligently at runtime, supporting functional design.

If Merlin was a 'wizard' he'd have no use for a walking stick lol ;D