TypeScript adds powerful type safety to JavaScript, enabling you to build more robust and maintainable applications. This guide explores advanced TypeScript patterns and best practices.
Advanced Type Features
Conditional Types
Create types that depend on other types:
type NonNullable<T> = T extends null | undefined ? never : T;
type ApiResponse<T> = T extends string
? { message: T }
: { data: T };
Mapped Types
Transform existing types:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Optional<T> = {
[P in keyof T]?: T[P];
};
Template Literal Types
Create types from string templates:
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'
Generics Deep Dive
Generic Constraints
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
Generic Utility Types
// Partial: Make all properties optional
type PartialUser = Partial<User>;
// Pick: Select specific properties
type UserEmail = Pick<User, 'email'>;
// Omit: Exclude specific properties
type UserWithoutId = Omit<User, 'id'>;
// Record: Create object type
type UserRoles = Record<string, boolean>;
Design Patterns
Factory Pattern
interface Product {
name: string;
price: number;
}
class ProductFactory {
static create<T extends Product>(type: new () => T): T {
return new type();
}
}
Builder Pattern
class QueryBuilder {
private query: string = '';
select(fields: string): this {
this.query += `SELECT ${fields}`;
return this;
}
from(table: string): this {
this.query += ` FROM ${table}`;
return this;
}
build(): string {
return this.query;
}
}
Singleton Pattern
class Database {
private static instance: Database;
private constructor() {}
static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
}
Advanced Utility Types
Extract and Exclude
type T0 = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // 'a'
type T1 = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
ReturnType and Parameters
function getUser(id: number): User {
return { id, name: 'John' };
}
type UserReturn = ReturnType<typeof getUser>; // User
type UserParams = Parameters<typeof getUser>; // [number]
Type Guards
User-Defined Type Guards
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function processValue(value: unknown) {
if (isString(value)) {
// TypeScript knows value is string here
console.log(value.toUpperCase());
}
}
Decorators (Experimental)
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey}`);
return original.apply(this, args);
};
}
class Calculator {
@Log
add(a: number, b: number): number {
return a + b;
}
}
Best Practices
1. Use Strict Mode
Enable strict type checking in tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true
}
}
2. Avoid Any
Use `unknown` instead of `any` when type is truly unknown:
function process(data: unknown) {
if (typeof data === 'string') {
// Type narrowing works
}
}
3. Use Interfaces for Objects
Use interfaces for object shapes, types for unions/intersections:
interface User {
id: number;
name: string;
}
type Status = 'active' | 'inactive';
4. Leverage Type Inference
Let TypeScript infer types when possible:
const users = ['John', 'Jane']; // string[]
const count = 42; // number
Mastering these advanced TypeScript patterns will help you write more type-safe, maintainable code. Practice with real projects and gradually incorporate these patterns into your workflow.