Shapes You'll See Everywhere
Generic Patterns
A consolidated tour of the most common generic patterns — containers, callbacks, fluent builders, mappers.
What you'll learn
- Recognize idiomatic generic shapes
- Pick patterns by intent
A roundup of generic shapes you’ll see — and use — constantly.
Container
class Box<T> {
constructor(public value: T) {}
} The simplest: a class or type holds a value of T.
Mapper
function map<T, U>(arr: T[], fn: (item: T) => U): U[] { /* ... */ } Transforms each element from T to U.
Wrapper That Adds Fields
function withId<T>(item: T): T & { id: string } {
return { ...item, id: crypto.randomUUID() };
} Returns the input’s type intersected with extra fields.
Result / Either
type Result<T, E = Error> =
| { ok: true; data: T }
| { ok: false; error: E }; Discriminated union over success and failure.
Fluent Builder
class Query<T extends Record<string, unknown> = {}> {
private filters: Partial<T> = {};
where<K extends string, V>(key: K, value: V): Query<T & Record<K, V>> {
(this.filters as any)[key] = value;
return this as any;
}
build(): T { return this.filters as T; }
}
const q = new Query()
.where("status", "active")
.where("limit", 10)
.build();
// q: { status: string; limit: number } Each where accumulates type info, ending with a fully typed
result. (The implementation uses any casts; the type-level magic
is in the signatures.)
Key-Value Helper
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
} Type-safe property access.
Generic Identity With Constraint
function freeze<T extends object>(x: T): Readonly<T> {
return Object.freeze(x);
} Same shape coming out, but read-only.
When To Stop Adding Generics
Generics serve type information. If you find yourself fighting the type system to add a sixth parameter, it’s usually a sign that the API needs to be split or simplified.
Up Next
The fanciest move — types that reference themselves.
Recursive Types →