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)
Thanks, @tonystpierre, this is really helpful and insightful.
Please keep sharing content like this!
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.
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.
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.
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).
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.
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.
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