Most TypeScript codebases use only a fraction of the type system's capabilities. Basic interfaces and enums are a start — but the patterns that truly eliminate runtime errors require going deeper.
Discriminated Unions
Discriminated unions model states that are mutually exclusive. They eliminate impossible states and enable exhaustive checking:
type Result<T> =
| { status: 'success'; data: T }
| { status: 'error'; message: string }
| { status: 'loading' };
function render(result: Result<User>) {
switch (result.status) {
case 'success': return result.data.name; // TS knows data exists
case 'error': return result.message;
case 'loading': return 'Loading...';
}
}
The satisfies Operator
Introduced in TypeScript 4.9, satisfies validates that a value conforms to a type while preserving the inferred type for downstream use:
const config = {
theme: 'dark',
lang: 'en',
} satisfies Record<string, string>;
// config.theme is inferred as 'dark', not string
config.theme.toUpperCase(); // works!
Template Literal Types
Template literal types create string types that follow a pattern — extremely useful for event systems, CSS-in-JS, and API route typing:
type EventName = 'click' | 'focus' | 'blur';
type Handler = 'on${Capitalize<EventName>}';
// Handler = 'onClick' | 'onFocus' | 'onBlur'
Branded Types
Branded types prevent mixing structurally identical but semantically different values — like user IDs and product IDs both typed as strings:
type UserId = string & { readonly brand: unique symbol };
type ProductId = string & { readonly brand: unique symbol };
function getUser(id: UserId) { ... }
const productId = 'abc' as ProductId;
getUser(productId); // TS Error! Type mismatch.
Strict Mode Is Not Optional
Enable strict: true in tsconfig.json from day one. It enables strictNullChecks, noImplicitAny, and several other checks that catch entire categories of bugs at compile time. Retrofitting strict mode onto an existing codebase is painful — starting strict is free.
Zod for Runtime Validation
TypeScript types are erased at runtime. Zod bridges the gap by providing runtime schema validation that infers TypeScript types — eliminating duplication between your type definitions and validation logic:
import { z } from 'zod';
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
age: z.number().min(18),
});
type User = z.infer<typeof UserSchema>;
Conclusion
The TypeScript type system is one of the most sophisticated in any mainstream language. The patterns here — discriminated unions, satisfies, branded types, and Zod — eliminate entire categories of bugs that unit tests often miss. Invest time learning them; the payoff is a codebase that fails at compile time rather than production.