Mastering TypeScript: Advanced Patterns and Best Practices
Deep dive into advanced TypeScript patterns, generics, utility types, and architectural best practices for building robust, type-safe applications.
Introduction
TypeScript has evolved from a simple type checker to a sophisticated type system capable of expressing complex patterns and guarantees. Mastering advanced TypeScript patterns enables you to build more robust, maintainable, and self-documenting code.
Why Advanced TypeScript?
Advanced TypeScript patterns help catch bugs at compile time, improve code reusability, and provide better IDE support with autocomplete and refactoring tools.
Generics Mastery
Generics are the cornerstone of reusable, type-safe code in TypeScript.
Basic to Advanced Generics
// Basic generic function
function identity<T>(value: T): T {
return value;
}
const num = identity<number>(42); // number
const str = identity("hello"); // Type inference: string
// Generic interface
interface Box<T> {
value: T;
getValue: () => T;
}
const numberBox: Box<number> = {
value: 42,
getValue: () => 42,
};
// Generic class
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);// Constrain generics with extends
interface HasId {
id: string;
}
function getById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
// Constrain to specific types
function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
// Keyof constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person = { name: "John", age: 30 };
const name = getProperty(person, "name"); // string
const age = getProperty(person, "age"); // number
// getProperty(person, "invalid"); // Error!
// Multiple constraints
function processList<T extends { length: number } & Iterable<T>>(
list: T
): number {
return list.length;
}// Multiple type parameters
function zip<T, U>(arr1: T[], arr2: U[]): [T, U][] {
const length = Math.min(arr1.length, arr2.length);
const result: [T, U][] = [];
for (let i = 0; i < length; i++) {
result.push([arr1[i], arr2[i]]);
}
return result;
}
const zipped = zip([1, 2, 3], ["a", "b", "c"]);
// Type: [number, string][]
// Inferring relationships between types
function map<T, U>(array: T[], fn: (item: T) => U): U[] {
return array.map(fn);
}
const numbers = [1, 2, 3];
const strings = map(numbers, n => n.toString()); // string[]
const doubled = map(numbers, n => n * 2); // number[]
// Generic constraints referring to each other
function copyFields<T extends U, U>(target: T, source: U): T {
return Object.assign(target, source);
}// Default type parameters
interface ApiResponse<T = unknown> {
data: T;
status: number;
message: string;
}
// Specific type
const userResponse: ApiResponse<User> = {
data: { id: "1", name: "John" },
status: 200,
message: "Success",
};
// Using default
const genericResponse: ApiResponse = {
data: { anything: "goes" },
status: 200,
message: "Success",
};
// Conditional defaults
type Container<T = string, U = T[]> = {
value: T;
values: U;
};
const stringContainer: Container = {
value: "hello",
values: ["a", "b", "c"],
};
const numberContainer: Container<number> = {
value: 42,
values: [1, 2, 3],
};Generic Best Practices
Use descriptive type parameter names (e.g., TUser, TResponse) for complex types, but T, U, V are fine for simple utilities.
Advanced Type Patterns
Mapped Types
Basic Mapping
Transform properties of a type:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Optional<T> = {
[P in keyof T]?: T[P];
};
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
interface User {
id: string;
name: string;
email: string;
}
type ReadonlyUser = Readonly<User>;
// { readonly id: string; readonly name: string; readonly email: string; }
type OptionalUser = Optional<User>;
// { id?: string; name?: string; email?: string; }Key Remapping
Rename keys while mapping:
// Add prefix to keys
type Prefixed<T, P extends string> = {
[K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};
type User = { name: string; age: number };
type PrefixedUser = Prefixed<User, "user">;
// { userName: string; userAge: number; }
// Filter keys
type FilterKeys<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
type Mixed = {
a: string;
b: number;
c: string;
d: boolean;
};
type OnlyStrings = FilterKeys<Mixed, string>;
// { a: string; c: string; }Nested Mapping
Recursively transform nested structures:
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object
? DeepPartial<T[P]>
: T[P];
};
interface NestedUser {
id: string;
profile: {
name: string;
settings: {
theme: string;
notifications: boolean;
};
};
}
type ReadonlyNestedUser = DeepReadonly<NestedUser>;
// All properties at all levels are readonlyConditional Types
// Basic conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Practical example: Extract return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser(): User {
return { id: "1", name: "John" };
}
type UserType = ReturnType<typeof getUser>; // User
// Exclude null and undefined
type NonNullable<T> = T extends null | undefined ? never : T;
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>; // string// Distributive conditional types
type ToArray<T> = T extends any ? T[] : never;
type StrOrNum = string | number;
type ArrayTypes = ToArray<StrOrNum>;
// string[] | number[] (not (string | number)[])
// Practical use: Filter union types
type ExtractStrings<T> = T extends string ? T : never;
type Mixed = string | number | boolean | string[];
type OnlyStrings = ExtractStrings<Mixed>; // string
// Extract specific types
type ExtractByType<T, U> = T extends U ? T : never;
type Values = "a" | 1 | "b" | 2 | true;
type StringValues = ExtractByType<Values, string>; // "a" | "b"
type NumberValues = ExtractByType<Values, number>; // 1 | 2// Infer type variables
type UnpackArray<T> = T extends (infer U)[] ? U : T;
type StringArray = UnpackArray<string[]>; // string
type NumberType = UnpackArray<number>; // number
// Infer function parameters
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
function createUser(name: string, age: number): User {
return { id: "1", name, age };
}
type CreateUserParams = Parameters<typeof createUser>;
// [name: string, age: number]
// Infer promise type
type Awaited<T> = T extends Promise<infer U> ? U : T;
type PromiseString = Awaited<Promise<string>>; // string
type Number = Awaited<number>; // number
// Complex inference
type FunctionReturn<T> = T extends {
(...args: any[]): infer R;
}
? R
: T extends {
new (...args: any[]): infer R;
}
? R
: never;// Combine multiple conditional types
type DeepExtractType<T, U> = {
[K in keyof T]: T[K] extends U
? T[K]
: T[K] extends object
? DeepExtractType<T[K], U>
: never;
};
// Function overload helper
type Overloads<T> = T extends {
(...args: infer A1): infer R1;
(...args: infer A2): infer R2;
(...args: infer A3): infer R3;
}
? [A1, A2, A3]
: never;
// Recursive conditional type
type Paths<T> = T extends object
? {
[K in keyof T]: K extends string
? T[K] extends object
? `${K}` | `${K}.${Paths<T[K]>}`
: `${K}`
: never;
}[keyof T]
: never;
type User = {
profile: {
name: string;
settings: {
theme: string;
};
};
};
type UserPaths = Paths<User>;
// "profile" | "profile.name" | "profile.settings" | "profile.settings.theme"Recursion Depth
TypeScript has a recursion depth limit (around 50 levels). For deeply nested types, consider iterative approaches or simplifying your type structure.
Utility Types Deep Dive
Built-in Utilities
// Partial - Make all properties optional
interface User {
id: string;
name: string;
email: string;
}
type PartialUser = Partial<User>;
// { id?: string; name?: string; email?: string; }
function updateUser(id: string, updates: Partial<User>): User {
// Only pass the fields you want to update
return { ...getUser(id), ...updates };
}
// Required - Make all properties required
type Config = {
apiUrl?: string;
timeout?: number;
retries?: number;
};
type RequiredConfig = Required<Config>;
// { apiUrl: string; timeout: number; retries: number; }
// Readonly - Make all properties readonly
type ReadonlyUser = Readonly<User>;
const user: ReadonlyUser = { id: "1", name: "John", email: "john@example.com" };
// user.name = "Jane"; // Error!// Pick - Select specific properties
type UserPreview = Pick<User, "id" | "name">;
// { id: string; name: string; }
// Omit - Exclude specific properties
type UserWithoutEmail = Omit<User, "email">;
// { id: string; name: string; }
// Record - Create object type with specific keys
type Roles = "admin" | "user" | "guest";
type Permissions = Record<Roles, string[]>;
// {
// admin: string[];
// user: string[];
// guest: string[];
// }
const permissions: Permissions = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"],
};
// Combine utilities
type EditableUser = Partial<Omit<User, "id">>;
// { name?: string; email?: string; } (id is excluded)// Extract - Extract types from union
type Status = "pending" | "approved" | "rejected" | "cancelled";
type ActiveStatus = Extract<Status, "pending" | "approved">;
// "pending" | "approved"
// Exclude - Exclude types from union
type InactiveStatus = Exclude<Status, "pending" | "approved">;
// "rejected" | "cancelled"
// NonNullable - Remove null and undefined
type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>;
// User
// ReturnType - Extract return type
function createUser(): User {
return { id: "1", name: "John", email: "john@example.com" };
}
type CreatedUser = ReturnType<typeof createUser>;
// User
// Parameters - Extract parameter types
function updateUser(id: string, data: Partial<User>): void {}
type UpdateParams = Parameters<typeof updateUser>;
// [id: string, data: Partial<User>]// Custom utility types
// Pick by type
type PickByType<T, U> = {
[P in keyof T as T[P] extends U ? P : never]: T[P];
};
interface Mixed {
id: string;
name: string;
age: number;
active: boolean;
}
type StringProps = PickByType<Mixed, string>;
// { id: string; name: string; }
// Deep partial
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object
? DeepPartial<T[P]>
: T[P];
};
// Mutable (opposite of Readonly)
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
// Get all function keys
type FunctionKeys<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
interface Service {
name: string;
count: number;
start: () => void;
stop: () => void;
}
type Methods = FunctionKeys<Service>;
// "start" | "stop"
// Promisify all methods
type Promisify<T> = {
[K in keyof T]: T[K] extends (...args: infer P) => infer R
? (...args: P) => Promise<R>
: T[K];
};
type AsyncService = Promisify<Service>;
// {
// name: string;
// count: number;
// start: () => Promise<void>;
// stop: () => Promise<void>;
// }Template Literal Types
Powerful string manipulation at the type level.
// Basic template literals
type Greeting = `Hello ${string}`;
const greeting1: Greeting = "Hello World"; // ✓
const greeting2: Greeting = "Hello TypeScript"; // ✓
// const greeting3: Greeting = "Hi there"; // ✗
// Union in template
type EventName = "click" | "focus" | "blur";
type EventHandler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
// Multiple unions create combinations
type Color = "red" | "blue";
type Size = "small" | "large";
type ColoredSize = `${Color}-${Size}`;
// "red-small" | "red-large" | "blue-small" | "blue-large"// Generate API endpoint types
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type Resource = "users" | "posts" | "comments";
type Endpoint = `${Lowercase<HTTPMethod>} /${Resource}`;
// "get /users" | "post /users" | "put /users" | "delete /users" | ...
// CSS properties
type CSSUnit = "px" | "em" | "rem" | "%";
type CSSValue<T extends string> = `${number}${T}`;
type Width = CSSValue<"px" | "%">;
// `${number}px` | `${number}%`
const width1: Width = "100px"; // ✓
const width2: Width = "50%"; // ✓
// Route params
type Routes = "/users/:id" | "/posts/:slug" | "/comments/:id";
type ExtractParams<T extends string> =
T extends `${infer _}:${infer Param}/${infer Rest}`
? Param | ExtractParams<`/${Rest}`>
: T extends `${infer _}:${infer Param}`
? Param
: never;
type RouteParams = ExtractParams<Routes>;
// "id" | "slug"// Email validation type
type Email = `${string}@${string}.${string}`;
const email1: Email = "user@example.com"; // ✓
const email2: Email = "test@domain.co.uk"; // ✓
// const email3: Email = "invalid.email"; // ✗
// Hex color
type HexColor = `#${string}`;
const color1: HexColor = "#FF0000"; // ✓
const color2: HexColor = "#00FF00"; // ✓
// const color3: HexColor = "FF0000"; // ✗
// SemVer pattern
type SemVer = `${number}.${number}.${number}`;
const version1: SemVer = "1.0.0"; // ✓
const version2: SemVer = "2.1.5"; // ✓
// Path validation
type AbsolutePath = `/${string}`;
type RelativePath = `./${string}` | `../${string}`;
type Path = AbsolutePath | RelativePath;
const path1: Path = "/home/user"; // ✓
const path2: Path = "./src/index.ts"; // ✓
const path3: Path = "../utils/helper.ts"; // ✓// Deep path access
type GetPath<T, Path extends string> =
Path extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? GetPath<T[Key], Rest>
: never
: Path extends keyof T
? T[Path]
: never;
type User = {
profile: {
name: string;
settings: {
theme: string;
notifications: boolean;
};
};
};
type Theme = GetPath<User, "profile.settings.theme">; // string
// Type-safe event system
type EventMap = {
"user:login": { userId: string; timestamp: number };
"user:logout": { userId: string };
"data:update": { id: string; data: unknown };
};
type EventName = keyof EventMap;
type EventPayload<T extends EventName> = EventMap[T];
class EventEmitter {
on<T extends EventName>(
event: T,
handler: (payload: EventPayload<T>) => void
): void {
// Implementation
}
emit<T extends EventName>(
event: T,
payload: EventPayload<T>
): void {
// Implementation
}
}
const emitter = new EventEmitter();
// Type-safe - payload matches event type
emitter.on("user:login", ({ userId, timestamp }) => {
console.log(`User ${userId} logged in at ${timestamp}`);
});
// Error - wrong payload type
// emitter.emit("user:login", { userId: "123" }); // Missing timestampTemplate Literal Use Cases
Template literal types are perfect for generating API routes, CSS class names, event names, and any string-based type validation.
Design Patterns
Builder Pattern
class QueryBuilder<T> {
private conditions: string[] = [];
private limitValue?: number;
private offsetValue?: number;
where(field: keyof T, operator: string, value: any): this {
this.conditions.push(`${String(field)} ${operator} ${value}`);
return this;
}
limit(n: number): this {
this.limitValue = n;
return this;
}
offset(n: number): this {
this.offsetValue = n;
return this;
}
build(): string {
let query = "SELECT * FROM table";
if (this.conditions.length > 0) {
query += " WHERE " + this.conditions.join(" AND ");
}
if (this.limitValue) {
query += ` LIMIT ${this.limitValue}`;
}
if (this.offsetValue) {
query += ` OFFSET ${this.offsetValue}`;
}
return query;
}
}
interface User {
id: number;
name: string;
email: string;
}
const query = new QueryBuilder<User>()
.where("name", "=", "'John'")
.where("email", "LIKE", "'%@example.com'")
.limit(10)
.offset(20)
.build();Factory Pattern
Define Product Interface
interface Database {
connect(): Promise<void>;
query(sql: string): Promise<any>;
disconnect(): Promise<void>;
}Implement Concrete Products
class PostgresDB implements Database {
async connect() {
console.log("Connecting to PostgreSQL...");
}
async query(sql: string) {
console.log(`Executing: ${sql}`);
return [];
}
async disconnect() {
console.log("Disconnecting from PostgreSQL");
}
}
class MongoDB implements Database {
async connect() {
console.log("Connecting to MongoDB...");
}
async query(sql: string) {
console.log(`Executing: ${sql}`);
return [];
}
async disconnect() {
console.log("Disconnecting from MongoDB");
}
}Create Factory
type DatabaseType = "postgres" | "mongodb" | "mysql";
class DatabaseFactory {
static create(type: DatabaseType): Database {
switch (type) {
case "postgres":
return new PostgresDB();
case "mongodb":
return new MongoDB();
case "mysql":
throw new Error("MySQL not implemented");
default:
const _exhaustive: never = type;
throw new Error(`Unknown database type: ${type}`);
}
}
}
// Usage
const db = DatabaseFactory.create("postgres");
await db.connect();
await db.query("SELECT * FROM users");
await db.disconnect();Repository Pattern
// Generic repository interface
interface IRepository<T> {
findById(id: string): Promise<T | null>;
findAll(): Promise<T[]>;
create(data: Omit<T, 'id'>): Promise<T>;
update(id: string, data: Partial<T>): Promise<T>;
delete(id: string): Promise<void>;
}
// Specific entity
interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
// User-specific repository interface
interface IUserRepository extends IRepository<User> {
findByEmail(email: string): Promise<User | null>;
findActive(): Promise<User[]>;
}class UserRepository implements IUserRepository {
constructor(private db: Database) {}
async findById(id: string): Promise<User | null> {
const result = await this.db.query(
"SELECT * FROM users WHERE id = $1",
[id]
);
return result[0] || null;
}
async findAll(): Promise<User[]> {
return await this.db.query("SELECT * FROM users");
}
async create(data: Omit<User, 'id'>): Promise<User> {
const result = await this.db.query(
"INSERT INTO users (name, email, created_at) VALUES ($1, $2, $3) RETURNING *",
[data.name, data.email, data.createdAt]
);
return result[0];
}
async update(id: string, data: Partial<User>): Promise<User> {
const fields = Object.keys(data)
.map((key, i) => `${key} = $${i + 2}`)
.join(", ");
const result = await this.db.query(
`UPDATE users SET ${fields} WHERE id = $1 RETURNING *`,
[id, ...Object.values(data)]
);
return result[0];
}
async delete(id: string): Promise<void> {
await this.db.query("DELETE FROM users WHERE id = $1", [id]);
}
async findByEmail(email: string): Promise<User | null> {
const result = await this.db.query(
"SELECT * FROM users WHERE email = $1",
[email]
);
return result[0] || null;
}
async findActive(): Promise<User[]> {
return await this.db.query(
"SELECT * FROM users WHERE active = true"
);
}
}// Service layer using repository
class UserService {
constructor(private userRepository: IUserRepository) {}
async registerUser(
name: string,
email: string
): Promise<User> {
// Check if user exists
const existing = await this.userRepository.findByEmail(email);
if (existing) {
throw new Error("User already exists");
}
// Create new user
const user = await this.userRepository.create({
name,
email,
createdAt: new Date(),
});
return user;
}
async getUserProfile(id: string): Promise<User> {
const user = await this.userRepository.findById(id);
if (!user) {
throw new Error("User not found");
}
return user;
}
async updateUserName(
id: string,
name: string
): Promise<User> {
return await this.userRepository.update(id, { name });
}
}
// Dependency injection
const db = DatabaseFactory.create("postgres");
const userRepository = new UserRepository(db);
const userService = new UserService(userRepository);
// Usage
const user = await userService.registerUser(
"John Doe",
"john@example.com"
);Repository Benefits
The repository pattern abstracts data access logic, making it easier to test, maintain, and swap data sources without changing business logic.
Type Guards and Narrowing
// typeof guards
function processValue(value: string | number) {
if (typeof value === "string") {
// TypeScript knows value is string here
return value.toUpperCase();
} else {
// TypeScript knows value is number here
return value.toFixed(2);
}
}
// Truthiness narrowing
function printLength(str: string | null | undefined) {
if (str) {
// str is string here
console.log(str.length);
} else {
// str is null | undefined here
console.log("No string provided");
}
}
// Equality narrowing
function compare(x: string | number, y: string | boolean) {
if (x === y) {
// x and y are both string here
console.log(x.toUpperCase(), y.toUpperCase());
}
}// instanceof guards
class ApiError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
}
}
class ValidationError extends Error {
constructor(public fields: string[]) {
super("Validation failed");
}
}
function handleError(error: Error) {
if (error instanceof ApiError) {
// error is ApiError
console.log(`API Error ${error.statusCode}: ${error.message}`);
} else if (error instanceof ValidationError) {
// error is ValidationError
console.log(`Validation failed for: ${error.fields.join(", ")}`);
} else {
// error is Error
console.log(`Unknown error: ${error.message}`);
}
}// User-defined type guards
interface User {
type: "user";
name: string;
email: string;
}
interface Admin {
type: "admin";
name: string;
permissions: string[];
}
// Type predicate
function isAdmin(account: User | Admin): account is Admin {
return account.type === "admin";
}
function getPermissions(account: User | Admin): string[] {
if (isAdmin(account)) {
// TypeScript knows account is Admin
return account.permissions;
} else {
// TypeScript knows account is User
return ["read"];
}
}
// Generic type guard
function isArrayOf<T>(
value: unknown,
check: (item: unknown) => item is T
): value is T[] {
return Array.isArray(value) && value.every(check);
}
function isString(value: unknown): value is string {
return typeof value === "string";
}
const data: unknown = ["a", "b", "c"];
if (isArrayOf(data, isString)) {
// data is string[]
data.forEach(str => console.log(str.toUpperCase()));
}// Discriminated unions
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
// shape is { kind: "circle"; radius: number }
return Math.PI * shape.radius ** 2;
case "rectangle":
// shape is { kind: "rectangle"; width: number; height: number }
return shape.width * shape.height;
case "triangle":
// shape is { kind: "triangle"; base: number; height: number }
return (shape.base * shape.height) / 2;
default:
// Exhaustiveness check
const _exhaustive: never = shape;
throw new Error(`Unknown shape: ${JSON.stringify(shape)}`);
}
}
// API response type
type ApiResponse<T> =
| { status: "success"; data: T }
| { status: "error"; error: string }
| { status: "loading" };
function handleResponse<T>(response: ApiResponse<T>): T | null {
switch (response.status) {
case "success":
return response.data;
case "error":
console.error(response.error);
return null;
case "loading":
console.log("Loading...");
return null;
}
}Best Practices
Enable Strict Mode
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true
}
}Prefer Type Inference
// Bad
const name: string = "John";
const age: number = 30;
// Good - Let TypeScript infer
const name = "John"; // string
const age = 30; // number
// Use explicit types for function returns
function getUser(): User { // ✓ Explicit return type
return { id: "1", name: "John", email: "john@example.com" };
}Use unknown over any
// Bad
function processData(data: any) {
return data.value; // No type safety
}
// Good
function processData(data: unknown) {
if (typeof data === "object" && data !== null && "value" in data) {
return (data as { value: unknown }).value;
}
throw new Error("Invalid data");
}Avoid Type Assertions
// Bad
const user = getUserData() as User;
// Good - Use type guards
const userData = getUserData();
if (isUser(userData)) {
const user = userData; // Type is narrowed
}Don't Skip Type Safety
Avoid using any or as any to bypass type errors. It defeats the purpose of TypeScript and can hide bugs.
Conclusion
Advanced TypeScript patterns enable you to write safer, more maintainable code. By mastering generics, conditional types, utility types, and design patterns, you can leverage TypeScript's full potential.
Remember: TypeScript is a tool to help you, not hinder you. Use these patterns where they add value, not everywhere.
Resources
Questions about TypeScript patterns? Let's discuss!