There's a point in every TypeScript developer's journey where you stop fighting the compiler and start working with it. These patterns are what got me there.
They're not just clever tricks. They solve real problems that come up when you're building production software.
Branded types for primitive safety
TypeScript treats string as string. So if you have a UserId and an OrderId, both strings, the compiler won't stop you from mixing them up. Branded types fix that.
type Brand<T, B> = T & { _brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
function getUser(id: UserId) {}
const orderId = "123" as OrderId;
getUser(orderId); // error, as it should be
Once you start using this in a fintech or any domain-heavy codebase, you'll wonder how you lived without it.
Mapped types for transforming shapes
Instead of manually creating variations of a type, let TypeScript do it for you.
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Optional<T> = {
[K in keyof T]?: T[K];
};
These are simple examples but the pattern scales. You can filter keys, remap them, add modifiers conditionally. Very useful when you're deriving API response types from your internal models.
Conditional types for smart generics
This is where TypeScript starts feeling like a programming language within a programming language.
type IsArray<T> = T extends any[] ? true : false;
type Flatten<T> = T extends Array<infer Item> ? Item : T;
// usage
type A = Flatten<string[]>; // string
type B = Flatten<number>; // number
Flatten is a real pattern I use when dealing with API responses that sometimes return an array and sometimes return a single item. Handle it at the type level and the rest of the code stays clean.
That's part one. Part two covers function overloads, template literal types, recursive types, and as const maps. Coming soon.
Top comments (0)