"Make Impossible States Impossible."
TypeScript is not just a linter or a tool for auto-complete. It is a modeling language. If your types allow a state that represents a bug, your types are buggy.
Most developers use "Bag of Optional Props" programming: { isLoading?: boolean, error?: string, data?: User }.
This allows a state where isLoading: false, error: null, and data: null exist simultaneously. This is a "Zombie State".
We fix this with Algebraic Data Types (Discriminated Unions).
02. Discriminated Unions (State Machines)
❌ The Bad Way
interface State {
isLoading: boolean;
error?: string;
data?: User;
}
// ⚠️ Impossible state allowed:
// { isLoading: false, error: undefined, data: undefined }
✅ The Safe Way
type State =
| { status: 'loading' }
| { status: 'error', error: string }
| { status: 'success', data: User }; // Data ONLY exists here
TypeScript acts as a control flow analyzer. Inside a `if(state.status === 'success')` block, it knows that `data` exists.
03. Template Literal DSLs
You can generate string types using template syntax. This is incredibly powerful for typed Event Emitters, CSS classes, or Internationalization keys.
type Entity = "User" | "Post" | "Comment";
type EventType = "created" | "deleted" | "updated";
// 🪄 Generates 9 possible string combinations automatically
type AppEvent = `${Entity}:${EventType}`;
// usage
function listen(event: AppEvent) {}
listen("User:created"); // ✅
listen("Post:archived"); // ❌ Error: "archived" is not in EventType
04. Conditional Types & Infer
Think of this as Ternary Operators for Types. `T extends U ? X : Y` means: "Does type T look like type U? If yes, use type X, otherwise Y."
// The 'infer' keyword extracts a part of the type definition type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never; function createUser() { return { id: 1, name: "Alice", role: "admin" } } // Magic: We extracted the return type without exporting an interface! type User = GetReturnType<typeof createUser>; // User = { id: number, name: string, role: string }
05. Branded Types (Domain Safety)
A number is not just a number. A `USD` amount should not be added to a `EUR` amount. A `UserId` should not be passable to a function expecting a `PostId`.
The Branding Pattern
// 1. Define the Brands
type Brand<K, T> = K & { __brand: T };
type USD = Brand<number, 'USD'>;
type EUR = Brand<number, 'EUR'>;
// 2. Usage
const wallet = 100 as USD;
const cost = 50 as EUR;
function pay(amount: USD) {}
pay(wallet); // ✅
pay(cost); // ❌ Error: Type 'EUR' is not assignable to type 'USD'
06. Advanced Utility Types
DeepPartial<T>
Recursively makes all properties optional.
type DeepPartial<T> = { [P in keyof T]?: DeepPartial<T[P]> };
Prettify<T>
Forces VS Code to show the computed type instead of the alias name.
type Prettify<T> = { [K in keyof T]: T[K] } & {};
07. Runtime Validation (Zod)
TypeScript handles compile time. Zod handles runtime. Instead of manually writing interfaces, write a Zod schema and infer the type from it.
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
age: z.number().min(18)
});
// ✨ Automatic Type Inference
type User = z.infer<typeof UserSchema>;
// { id: string; email: string; age: number; }
08. Compiler Performance
Warning: Excessive recursion or massive unions (10k+ items) will slay your Intellisense speed. Use `interface` extends where possible instead of intersection types ` & ` for object shapes, as interfaces cache better.
09. The Type Visualizer
Below is a visualization of how Discriminated Unions work in a real-world scenario (a Shape Sorter). Notice how the properties available change based on the "Kind".