// Full autocomplete on event names, parameter types, and callback signatures
Note This pattern maps event names to callback signatures. Parameters<T[K]> extracts the argument tuple so emit() is fully typed. This is the foundation used by libraries like mitt, EventEmitter3, and socket.io.
typed event emitterevent bustype safe eventspub sub typedemit pattern
Type-Safe Builder Pattern
syntax
class Builder<T> {
set<K extends keyof T>(key: K, value: T[K]): this { ... }
build(): T { ... }
}
example
interface EmailMessage {
to: string;
subject: string;
body: string;
cc?: string[];
priority?: "low" | "normal" | "high";
}
class EmailBuilder {
private message: Partial<EmailMessage> = {};
to(address: string): this {
this.message.to = address;
returnthis;
}
subject(text: string): this {
this.message.subject = text;
returnthis;
}
body(html: string): this {
this.message.body = html;
returnthis;
}
cc(addresses: string[]): this {
this.message.cc = addresses;
returnthis;
}
priority(level: EmailMessage["priority"]): this {
this.message.priority = level;
returnthis;
}
build(): EmailMessage {
if (!this.message.to || !this.message.subject || !this.message.body) {
thrownew Error("to, subject, and body are required");
}
returnthis.message as EmailMessage;
}
}
const email = new EmailBuilder()
.to("[email protected]")
.subject("Deploy complete")
.body("<p>All checks passed.</p>")
.priority("high")
.build();
output
// Fluent API with full type safety and autocomplete at each step
Note Returning 'this' instead of the class name enables proper chaining even in subclasses. For compile-time enforcement of required fields (instead of runtime throws), use a step builder pattern with generics that track which fields have been set.
function assertNever(value: never): never {
thrownew Error(`Unexpected: ${value}`);
}
example
type PaymentMethod = "card" | "bank" | "crypto" | "paypal";
function processPayment(method: PaymentMethod): string {
switch (method) {
case"card":
return"Processing card payment";
case"bank":
return"Processing bank transfer";
case"crypto":
return"Processing crypto payment";
case"paypal":
return"Processing PayPal";
default:
// If a new member is added to PaymentMethod,// this line causes a compile errorconst _exhaustive: never = method;
thrownew Error(`Unknown method: ${_exhaustive}`);
}
}
output
// Adding a fifth payment method causes a compile error at the never assignment
Note The never trick ensures you handle every union member. When a new member is added, TypeScript sees it is not handled and cannot assign it to never. This is critical for state machines, reducers, and any logic that must cover all cases. Some teams extract this into a shared assertNever utility function.
exhaustive switchassertNeverhandle all casesmissing case errorexhaustive check
const Assertions
syntax
const value = expression asconst;
example
// Without as const: types are widenedconst config = { apiUrl: "https://api.example.com", retries: 3 };
// { apiUrl: string; retries: number }// With as const: literal types + readonlyconst frozenConfig = {
apiUrl: "https://api.example.com",
retries: 3,
features: ["search", "export"],
} asconst;
// { readonly apiUrl: "https://api.example.com"; readonly retries: 3; readonly features: readonly ["search", "export"] }// Deriving a union type from const valuesconst ROLES = ["admin", "editor", "viewer"] asconst;
type Role = (typeof ROLES)[number];
// "admin" | "editor" | "viewer"
output
// as const freezes the type to its most specific literal form
Note as const makes all properties readonly and infers the narrowest possible literal types. It applies recursively to nested objects and arrays. This is the idiomatic way to derive string literal unions from runtime arrays, replacing enums in many cases.
as constconst assertionliteral inferencereadonly objectderive union from array
Result Type Pattern
syntax
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
example
type Result<T, E = string> =
| { ok: true; value: T }
| { ok: false; error: E };
function parseAge(input: string): Result<number> {
const num = parseInt(input, 10);
if (isNaN(num)) return { ok: false, error: "Not a valid number" };
if (num < 0 || num > 150) return { ok: false, error: "Age out of range" };
return { ok: true, value: num };
}
const result = parseAge("25");
if (result.ok) {
console.log(`Age: ${result.value}`); // narrowed to { ok: true; value: number }
} else {
console.log(`Invalid: ${result.error}`); // narrowed to { ok: false; error: string }
}
output
// Discriminated union forces callers to handle both success and failure
Note The Result pattern replaces throwing exceptions with explicit return types. Callers cannot forget to handle the error case because TypeScript requires narrowing before accessing .value or .error. This is inspired by Rust's Result<T, E> type.
result typeerror handling patterneither typesuccess failureno throw pattern
Type-Safe Runtime Validation
syntax
// Using infer with validation librariestype Inferred = z.infer<typeof schema>;
example
// Zod-style schema → derived type (no duplication)import { z } from"zod";
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150).optional(),
role: z.enum(["admin", "user", "guest"]),
});
// Derive the TypeScript type from the schematype User = z.infer<typeof UserSchema>;
// { id: string; name: string; email: string; age?: number; role: "admin" | "user" | "guest" }function createUser(raw: unknown): User {
return UserSchema.parse(raw); // throws if invalid
}
output
// Single schema defines both runtime validation and compile-time type
Note Libraries like Zod, Valibot, and ArkType let you define a schema once and derive the TypeScript type from it. This eliminates the common problem of types and validation getting out of sync. z.infer<typeof schema> is a powerful pattern that keeps your types as the single source of truth.
zodruntime validationschema typeinfer from schematype safe parsingvalidate unknown
Type-Safe Action / Reducer Pattern
syntax
type Action = { type: "name"; payload: Type } | ...;
function reducer(state: State, action: Action): State { ... }
// Each case narrows the action type, giving access to case-specific fields
Note This is the standard pattern for Redux, useReducer, and state machines. The 'type' property serves as the discriminant. TypeScript narrows automatically in each case, so action.amount is only available in increment/decrement cases. Add a default never check for exhaustiveness.
reducer patternaction typeredux typeuseReducer typedstate machinedispatch type