// 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
interfaceEmailMessage{
to: string;
subject: string;
body: string;
cc?: string[];
priority?:"low"|"normal"|"high";}classEmailBuilder{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){thrownewError("to, subject, and body are required");}returnthis.messageasEmailMessage;}}const email =newEmailBuilder().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.
functionassertNever(value: never): never {thrownewError(`Unexpected: ${value}`);}
Example
typePaymentMethod="card"|"bank"|"crypto"|"paypal";functionprocessPayment(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;thrownewError(`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 valuesconstROLES=["admin","editor","viewer"]asconst;typeRole=(typeofROLES)[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
typeResult<T,E= string>=|{ ok:true; value:T}|{ ok:false; error:E};functionparseAge(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 librariestypeInferred= z.infer<typeof schema>;
Example
// Zod-style schema → derived type (no duplication)import{ z }from"zod";constUserSchema= 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 schematypeUser= z.infer<typeofUserSchema>;// { id: string; name: string; email: string; age?: number; role: "admin" | "user" | "guest" }functioncreateUser(raw: unknown):User{returnUserSchema.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
// 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.