| name | typescript-patterns |
| description | TypeScript patterns, advanced typing, and type safety. Use when user asks to "write TypeScript generics", "create utility types", "fix TypeScript errors", "type a complex object", "use conditional types", "create type guards", "improve TypeScript types", "design type-safe APIs", "use mapped types", "create branded types", "infer types", "type narrowing", "type variance", "overload signatures", "type assertion", or mentions TypeScript patterns, advanced typing, generic constraints, discriminated unions, template literal types, or type inference. |
| license | MIT |
| metadata | {"author":"1mangesh1","version":"1.0.0","tags":["typescript","types","generics","utility-types","type-safety","patterns"]} |
Advanced TypeScript Patterns
Production-grade TypeScript patterns for building type-safe, maintainable, and scalable applications. This skill covers generics, utility types, conditional types, discriminated unions, branded types, and more.
1. Advanced Generics with Constraints
Basic Generic Constraints
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength("hello");
getLength([1, 2, 3]);
Multiple Constraints with Intersection
interface HasId {
id: string;
}
interface HasTimestamp {
createdAt: Date;
updatedAt: Date;
}
function updateEntity<T extends HasId & HasTimestamp>(
entity: T,
updates: Partial<Omit<T, "id" | "createdAt">>
): T {
return { ...entity, ...updates, updatedAt: new Date() };
}
keyof Constraint Pattern
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30, email: "alice@example.com" };
const name = getProperty(user, "name");
const age = getProperty(user, "age");
Generic Factory Pattern
interface Constructor<T> {
new (...args: any[]): T;
}
function createInstance<T>(ctor: Constructor<T>, ...args: any[]): T {
return new ctor(...args);
}
class UserService {
constructor(public apiUrl: string) {}
}
const service = createInstance(UserService, "https://api.example.com");
2. Built-in Utility Types
Pick, Omit, Partial, Required
interface User {
id: string;
name: string;
email: string;
avatar?: string;
role: "admin" | "user" | "moderator";
preferences: {
theme: "light" | "dark";
notifications: boolean;
};
}
type UserSummary = Pick<User, "id" | "name" | "avatar">;
type PublicUser = Omit<User, "email" | "preferences">;
type UserUpdate = Partial<Omit<User, "id">>;
type CompleteUser = Required<User>;
Record, Extract, Exclude
type UserRole = "admin" | "user" | "moderator";
type RolePermissions = Record<UserRole, string[]>;
const permissions: RolePermissions = {
admin: ["read", "write", "delete", "manage-users"],
user: ["read", "write"],
moderator: ["read", "write", "delete"],
};
type NumericEvent = Extract<"click" | "scroll" | "keypress" | 42, string>;
type NonAdminRole = Exclude<UserRole, "admin">;
ReturnType, Parameters, InstanceType
function fetchUser(id: string, options?: { includeProfile: boolean }) {
return { id, name: "Alice", email: "alice@example.com" };
}
type FetchUserReturn = ReturnType<typeof fetchUser>;
type FetchUserParams = Parameters<typeof fetchUser>;
class ApiClient {
baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
}
type ApiClientInstance = InstanceType<typeof ApiClient>;
3. Conditional Types and the infer Keyword
Basic Conditional Types
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">;
type B = IsString<42>;
Extracting Types with infer
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type Result = UnwrapPromise<Promise<string>>;
type Plain = UnwrapPromise<number>;
type ElementOf<T> = T extends (infer E)[] ? E : never;
type Item = ElementOf<string[]>;
type Nested = ElementOf<number[]>;
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type Arg = FirstArg<(name: string, age: number) => void>;
Distributive Conditional Types
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>;
Recursive Conditional Types
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
type Deep = DeepUnwrap<Promise<Promise<Promise<string>>>>;
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
interface Config {
server: {
host: string;
port: number;
ssl: {
enabled: boolean;
cert: string;
};
};
}
type PartialConfig = DeepPartial<Config>;
4. Mapped Types and Template Literal Types
Custom Mapped Types
type Immutable<T> = {
readonly [K in keyof T]: T[K] extends object ? Immutable<T[K]> : T[K];
};
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
type Prefixed<T, P extends string> = {
[K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};
interface FormData {
name: string;
email: string;
}
type OnChangeHandlers = Prefixed<FormData, "onChange">;
Template Literal Types
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
type APIVersion = "v1" | "v2";
type Resource = "users" | "posts" | "comments";
type APIRoute = `/${APIVersion}/${Resource}`;
type DOMEvent = "click" | "focus" | "blur" | "input";
type EventHandler = `on${Capitalize<DOMEvent>}`;
type CSSUnit = "px" | "rem" | "em" | "vh" | "vw" | "%";
type CSSValue = `${number}${CSSUnit}`;
const width: CSSValue = "100px";
const height: CSSValue = "50vh";
Key Remapping with as
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
type StringKeysOnly<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Mixed {
name: string;
age: number;
email: string;
active: boolean;
}
type OnlyStrings = StringKeysOnly<Mixed>;
5. Discriminated Unions and Exhaustive Checking
Basic Discriminated Union
interface LoadingState {
status: "loading";
}
interface SuccessState<T> {
status: "success";
data: T;
}
interface ErrorState {
status: "error";
error: {
message: string;
code: number;
};
}
type RequestState<T> = LoadingState | SuccessState<T> | ErrorState;
function renderState<T>(state: RequestState<T>): string {
switch (state.status) {
case "loading":
return "Loading...";
case "success":
return `Data: ${JSON.stringify(state.data)}`;
case "error":
return `Error ${state.error.code}: ${state.error.message}`;
}
}
Exhaustive Checking with never
function assertNever(value: never): never {
throw new Error(`Unhandled discriminated union member: ${JSON.stringify(value)}`);
}
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
default:
return assertNever(shape);
}
}
Action/Event Pattern (Redux-style)
type Action =
| { type: "ADD_TODO"; payload: { text: string } }
| { type: "TOGGLE_TODO"; payload: { id: number } }
| { type: "REMOVE_TODO"; payload: { id: number } }
| { type: "SET_FILTER"; payload: { filter: "all" | "active" | "completed" } };
interface TodoState {
todos: Array<{ id: number; text: string; completed: boolean }>;
filter: "all" | "active" | "completed";
}
function todoReducer(state: TodoState, action: Action): TodoState {
switch (action.type) {
case "ADD_TODO":
return {
...state,
todos: [
...state.todos,
{ id: Date.now(), text: action.payload.text, completed: false },
],
};
case "TOGGLE_TODO":
return {
...state,
todos: state.todos.map((t) =>
t.id === action.payload.id ? { ...t, completed: !t.completed } : t
),
};
case "REMOVE_TODO":
return {
...state,
todos: state.todos.filter((t) => t.id !== action.payload.id),
};
case "SET_FILTER":
return { ...state, filter: action.payload.filter };
default:
return assertNever(action as never);
}
}
6. Type Guards and Type Narrowing
Custom Type Guard Functions
interface Cat {
type: "cat";
purrs: boolean;
}
interface Dog {
type: "dog";
barks: boolean;
}
type Animal = Cat | Dog;
function isCat(animal: Animal): animal is Cat {
return animal.type === "cat";
}
function handleAnimal(animal: Animal) {
if (isCat(animal)) {
console.log(animal.purrs);
} else {
console.log(animal.barks);
}
}
Assertion Functions
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error(`Expected string, got ${typeof value}`);
}
}
function processInput(input: unknown) {
assertIsString(input);
console.log(input.toUpperCase());
}
in Operator Narrowing
interface APIResponse {
data: unknown;
}
interface APIError {
error: string;
statusCode: number;
}
function handleResponse(res: APIResponse | APIError) {
if ("error" in res) {
console.error(`Error ${res.statusCode}: ${res.error}`);
} else {
console.log(res.data);
}
}
Narrowing with satisfies
type Color = "red" | "green" | "blue";
type ColorMap = Record<Color, string | [number, number, number]>;
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
} satisfies ColorMap;
const redChannel = palette.red[0];
7. Branded / Opaque Types for Type Safety
Preventing Primitive Type Confusion
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
type Email = Brand<string, "Email">;
function createUserId(id: string): UserId {
if (!id.match(/^usr_[a-zA-Z0-9]{12}$/)) {
throw new Error("Invalid user ID format");
}
return id as UserId;
}
function createEmail(email: string): Email {
if (!email.includes("@")) {
throw new Error("Invalid email format");
}
return email as Email;
}
function sendEmail(to: Email, subject: string): void {
}
function getUser(id: UserId): void {
}
const userId = createUserId("usr_abc123def456");
const email = createEmail("alice@example.com");
sendEmail(email, "Welcome!");
Branded Numeric Types
type Celsius = Brand<number, "Celsius">;
type Fahrenheit = Brand<number, "Fahrenheit">;
type Kilometers = Brand<number, "Kilometers">;
type Miles = Brand<number, "Miles">;
function celsiusToFahrenheit(c: Celsius): Fahrenheit {
return ((c * 9) / 5 + 32) as Fahrenheit;
}
function milesToKilometers(m: Miles): Kilometers {
return (m * 1.60934) as Kilometers;
}
const temp = 100 as Celsius;
const converted = celsiusToFahrenheit(temp);
8. Builder Pattern with Types
Type-Safe Builder
interface QueryConfig {
table: string;
columns: string[];
where: string[];
orderBy: string | null;
limit: number | null;
}
type RequiredFields = "table" | "columns";
class QueryBuilder<T extends Partial<QueryConfig> = {}> {
private config: Partial<QueryConfig> = {};
from<Table extends string>(
table: Table
): QueryBuilder<T & { table: Table }> {
this.config.table = table;
return this as any;
}
select(
...columns: string[]
): QueryBuilder<T & { columns: string[] }> {
this.config.columns = columns;
return this as any;
}
where(condition: string): QueryBuilder<T & { where: string[] }> {
this.config.where = [...(this.config.where ?? []), condition];
return this as any;
}
orderBy(column: string): this {
this.config.orderBy = column;
return this;
}
limit(count: number): this {
this.config.limit = count;
return this;
}
build(
this: QueryBuilder<Pick<QueryConfig, RequiredFields>>
): QueryConfig {
return {
table: this.config.table!,
columns: this.config.columns!,
where: this.config.where ?? [],
orderBy: this.config.orderBy ?? null,
limit: this.config.limit ?? null,
};
}
}
const query = new QueryBuilder()
.from("users")
.select("id", "name", "email")
.where("active = true")
.orderBy("name")
.limit(10)
.build();
9. Runtime Validation with Zod
Schema Definition and Inference
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", "moderator"]),
tags: z.array(z.string()).default([]),
metadata: z.record(z.string(), z.unknown()).optional(),
createdAt: z.coerce.date(),
});
type User = z.infer<typeof UserSchema>;
function createUser(input: unknown): User {
return UserSchema.parse(input);
}
function tryCreateUser(input: unknown): User | null {
const result = UserSchema.safeParse(input);
if (result.success) {
return result.data;
}
console.error("Validation errors:", result.error.flatten());
return null;
}
API Request/Response Validation
const CreateUserRequest = UserSchema.omit({ id: true, createdAt: true });
const UpdateUserRequest = CreateUserRequest.partial();
const APIResponse = <T extends z.ZodType>(dataSchema: T) =>
z.object({
success: z.boolean(),
data: dataSchema,
meta: z
.object({
page: z.number(),
total: z.number(),
})
.optional(),
});
const UserListResponse = APIResponse(z.array(UserSchema));
type UserListResponse = z.infer<typeof UserListResponse>;
10. Common TypeScript Errors and Fixes
TS2322: Type 'X' is not assignable to type 'Y'
const status: "active" | "inactive" = "active";
let mutable = status;
let mutable: typeof status = status;
const statuses = ["active", "inactive"] as const;
type Status = (typeof statuses)[number];
TS2345: Argument of type 'X' is not assignable to parameter of type 'Y'
interface Options {
timeout: number;
}
function configure(opts: Options) {}
const opts = { timeout: 5000, retries: 3 };
configure(opts);
TS7053: Element implicitly has an 'any' type (index access)
const config: Record<string, unknown> = {};
const value = config["key"];
function getString(obj: Record<string, unknown>, key: string): string {
const val = obj[key];
if (typeof val !== "string") {
throw new Error(`Expected string at key "${key}"`);
}
return val;
}
TS2339: Property does not exist on type
type Result = { ok: true; value: string } | { ok: false; error: string };
function handle(result: Result) {
if (result.ok) {
console.log(result.value);
} else {
console.log(result.error);
}
}
11. Declaration Merging and Module Augmentation
Interface Merging
declare module "express" {
interface Request {
user?: {
id: string;
role: string;
};
requestId: string;
}
}
import { Request } from "express";
function handler(req: Request) {
console.log(req.user?.id);
console.log(req.requestId);
}
Global Augmentation
declare global {
interface Window {
__APP_CONFIG__: {
apiUrl: string;
environment: "development" | "staging" | "production";
};
}
interface Array<T> {
groupBy<K extends string>(fn: (item: T) => K): Record<K, T[]>;
}
}
export {};
Namespace Merging
function currency(amount: number): string;
function currency(amount: number, code: string): string;
function currency(amount: number, code?: string): string {
return `${(code ?? "USD")} ${amount.toFixed(2)}`;
}
namespace currency {
export const codes = ["USD", "EUR", "GBP", "JPY"] as const;
export type Code = (typeof codes)[number];
export function convert(amount: number, from: Code, to: Code): number {
return amount;
}
}
currency(42);
currency.convert(100, "USD", "EUR");
12. Strict Mode Best Practices
Recommended tsconfig.json Strict Settings
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"noPropertyAccessFromIndexSignature": true,
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"lib": ["ES2022"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
Handling Strict Null Checks
function getUser(id: string): User | undefined {
return users.get(id);
}
function getUserName(id: string): string {
const user = getUser(id);
if (!user) {
throw new Error(`User ${id} not found`);
}
return user.name;
}
function unsafeGetName(id: string): string {
return getUser(id)!.name;
}
function safeGetName(id: string): string {
return getUser(id)?.name ?? "Anonymous";
}
noUncheckedIndexedAccess Pattern
const items: string[] = ["a", "b", "c"];
const first = items[0];
if (first !== undefined) {
console.log(first.toUpperCase());
}
function at<T>(arr: T[], index: number): T {
const item = arr[index];
if (item === undefined) {
throw new RangeError(`Index ${index} out of bounds`);
}
return item;
}
Quick Reference: When to Use What
| Pattern | Use Case |
|---|
| Generics | Reusable functions/classes that work with multiple types |
| Utility types | Transform existing types (pick fields, make optional, etc.) |
| Conditional types | Types that depend on other types at compile time |
| Mapped types | Transform all properties of a type systematically |
| Template literals | Generate string literal union types |
| Discriminated unions | Model states, events, actions with tagged variants |
| Type guards | Runtime checks that narrow types for the compiler |
| Branded types | Prevent mixing up primitives (UserId vs OrderId) |
| Builder pattern | Enforce required steps at compile time |
| Zod schemas | Runtime validation with automatic type inference |
| Declaration merging | Extend third-party or global types |
| Strict mode | Catch more bugs at compile time |
References