Sign In Create Account
Best Practices

TypeScript Best Practices

Advanced patterns and techniques for leveraging TypeScript's powerful type system effectively.

Generic Constraints and Conditional Types

Use generics with constraints to create flexible, reusable components while maintaining type safety.

Generic Constraints

// ✅ Generic constraints for better type safety
interface Identifiable {
  id: string;
}

interface Timestamped {
  createdAt: Date;
  updatedAt: Date;
}

// Constrain generic to have specific properties
function updateEntity<T extends Identifiable>(
  entity: T, 
  updates: Partial<Omit<T, 'id'>>
): T {
  return { ...entity, ...updates };
}

// Multiple constraints
function createAuditLog<T extends Identifiable & Timestamped>(
  entity: T,
  action: string
): AuditLog {
  return {
    entityId: entity.id,
    action,
    timestamp: new Date(),
    entityType: entity.constructor.name
  };
}

// ✅ Conditional types for advanced type manipulation
type ApiResponse<T> = T extends string 
  ? { message: T }
  : T extends number
  ? { count: T }
  : { data: T };

// Usage examples
type StringResponse = ApiResponse<string>; // { message: string }
type NumberResponse = ApiResponse<number>; // { count: number }
type UserResponse = ApiResponse<User>; // { data: User }

// ✅ Mapped types with conditions
type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type MakeRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };

interface User {
  id: string;
  name: string;
  email: string;
  avatar?: string;
}

type UserWithOptionalEmail = MakeOptional<User, 'email'>; // email becomes optional
type UserWithRequiredAvatar = MakeRequired<User, 'avatar'>; // avatar becomes required

// ✅ Utility type for deep readonly
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

const config: DeepReadonly<AppConfig> = {
  api: {
    baseUrl: 'https://api.example.com',
    timeout: 5000
  }
}; // All nested properties are readonly

Utility Types and Advanced Patterns

Leverage TypeScript's built-in utility types and create custom ones for common patterns.

Utility Types in Action

// ✅ Built-in utility types
interface User {
  id: string;
  name: string;
  email: string;
  password: string;
  role: 'admin' | 'user';
  createdAt: Date;
  updatedAt: Date;
}

// Pick specific properties
type UserPublic = Pick<User, 'id' | 'name' | 'email' | 'role'>;

// Omit sensitive properties
type UserSafe = Omit<User, 'password'>;

// Make properties optional
type UserUpdate = Partial<Pick<User, 'name' | 'email' | 'role'>>;

// Create user input type
type CreateUserInput = Omit<User, 'id' | 'createdAt' | 'updatedAt'>;

// ✅ Custom utility types
type NonEmptyArray<T> = [T, ...T[]];

// Extract function return type
type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;

async function fetchUser(id: string): Promise<User> {
  // implementation
  return {} as User;
}

type FetchUserResult = ReturnTypeOf<typeof fetchUser>; // Promise<User>

// ✅ Builder pattern with fluent interface
class QueryBuilder<T> {
  private conditions: string[] = [];
  private selectFields: string[] = [];
  private orderByField?: string;

  select<K extends keyof T>(...fields: K[]): QueryBuilder<Pick<T, K>> {
    this.selectFields = fields as string[];
    return this as any;
  }

  where(condition: string): QueryBuilder<T> {
    this.conditions.push(condition);
    return this;
  }

  orderBy<K extends keyof T>(field: K): QueryBuilder<T> {
    this.orderByField = field as string;
    return this;
  }

  build(): string {
    let query = `SELECT ${this.selectFields.join(', ')} FROM table`;
    if (this.conditions.length > 0) {
      query += ` WHERE ${this.conditions.join(' AND ')}`;
    }
    if (this.orderByField) {
      query += ` ORDER BY ${this.orderByField}`;
    }
    return query;
  }
}

// Usage with type safety
const query = new QueryBuilder<User>()
  .select('id', 'name', 'email') // TypeScript knows these are valid User keys
  .where('active = true')
  .orderBy('createdAt')
  .build();

Error Handling and Validation

Implement robust error handling patterns with proper TypeScript typing.

Error Handling Patterns

// ✅ Result pattern for error handling
type Result<T, E = Error> = 
  | { success: true; data: T }
  | { success: false; error: E };

async function fetchUserSafe(id: string): Promise<Result<User, string>> {
  try {
    const user = await fetchUser(id);
    return { success: true, data: user };
  } catch (error) {
    return { 
      success: false, 
      error: error instanceof Error ? error.message : 'Unknown error' 
    };
  }
}

// Usage with type safety
async function handleUser(id: string): Promise<void> {
  const result = await fetchUserSafe(id);
  
  if (result.success) {
    console.log(result.data.name); // TypeScript knows data is User
  } else {
    console.error(result.error); // TypeScript knows error is string
  }
}

// ✅ Custom error classes with proper typing
abstract class AppError extends Error {
  abstract readonly statusCode: number;
  abstract readonly isOperational: boolean;
}

class ValidationError extends AppError {
  readonly statusCode = 400;
  readonly isOperational = true;

  constructor(
    public readonly field: string,
    message: string
  ) {
    super(message);
    this.name = 'ValidationError';
  }
}

class NotFoundError extends AppError {
  readonly statusCode = 404;
  readonly isOperational = true;

  constructor(resource: string, id: string) {
    super(`${resource} with ID ${id} not found`);
    this.name = 'NotFoundError';
  }
}

// ✅ Runtime validation with type guards
function validateUser(data: unknown): User {
  if (!isObject(data)) {
    throw new ValidationError('user', 'User data must be an object');
  }

  if (!isString(data.name) || data.name.trim().length === 0) {
    throw new ValidationError('name', 'Name is required and must be a non-empty string');
  }

  if (!isString(data.email) || !isValidEmail(data.email)) {
    throw new ValidationError('email', 'Valid email is required');
  }

  return data as User; // Safe cast after validation
}

function isObject(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null;
}

function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function isValidEmail(email: string): boolean {
  return /^[^s@]+@[^s@]+.[^s@]+$/.test(email);
}

// ✅ Async error handling with proper types
type AsyncResult<T> = Promise<Result<T, AppError>>;

class UserService {
  async createUser(userData: CreateUserInput): AsyncResult<User> {
    try {
      const validatedData = validateUser(userData);
      const user = await this.saveUser(validatedData);
      return { success: true, data: user };
    } catch (error) {
      if (error instanceof AppError) {
        return { success: false, error };
      }
      return { 
        success: false, 
        error: new ValidationError('general', 'Failed to create user') 
      };
    }
  }

  private async saveUser(userData: CreateUserInput): Promise<User> {
    // Database save implementation
    return {} as User;
  }
}

Advanced Function Patterns

Implement sophisticated function patterns with proper type safety and flexibility.

Advanced Function Patterns

// ✅ Function overloads for flexible APIs
function createElement(tag: 'img'): HTMLImageElement;
function createElement(tag: 'input'): HTMLInputElement;
function createElement(tag: 'div'): HTMLDivElement;
function createElement(tag: string): HTMLElement;
function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}

// Usage - TypeScript knows the specific return type
const img = createElement('img'); // HTMLImageElement
const input = createElement('input'); // HTMLInputElement
const div = createElement('div'); // HTMLDivElement

// ✅ Higher-order functions with proper typing
type EventHandler<T> = (event: T) => void;
type UnsubscribeFunction = () => void;

function createEventEmitter<TEvents extends Record<string, any>>() {
  const listeners: { [K in keyof TEvents]?: EventHandler<TEvents[K]>[] } = {};

  return {
    on<K extends keyof TEvents>(
      event: K, 
      handler: EventHandler<TEvents[K]>
    ): UnsubscribeFunction {
      if (!listeners[event]) {
        listeners[event] = [];
      }
      listeners[event]!.push(handler);

      return () => {
        const handlers = listeners[event];
        if (handlers) {
          const index = handlers.indexOf(handler);
          if (index > -1) {
            handlers.splice(index, 1);
          }
        }
      };
    },

    emit<K extends keyof TEvents>(event: K, data: TEvents[K]): void {
      const handlers = listeners[event];
      if (handlers) {
        handlers.forEach(handler => handler(data));
      }
    }
  };
}

// Usage with type safety
interface AppEvents {
  userLogin: { userId: string; timestamp: Date };
  userLogout: { userId: string };
  dataUpdate: { table: string; id: string; changes: any };
}

const eventEmitter = createEventEmitter<AppEvents>();

// TypeScript knows the event data type
eventEmitter.on('userLogin', (data) => {
  console.log(`User ${data.userId} logged in at ${data.timestamp}`);
});

// ✅ Decorator pattern with proper typing
function memoize<T extends (...args: any[]) => any>(
  target: T
): T {
  const cache = new Map();
  
  return ((...args: Parameters<T>) => {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }
    
    const result = target(...args);
    cache.set(key, result);
    return result;
  }) as T;
}

// Usage
const expensiveCalculation = memoize((x: number, y: number): number => {
  console.log('Calculating...');
  return x * y + Math.random();
});

// ✅ Currying with proper type inference
function curry<A, B, C>(
  fn: (a: A, b: B) => C
): (a: A) => (b: B) => C;
function curry<A, B, C, D>(
  fn: (a: A, b: B, c: C) => D
): (a: A) => (b: B) => (c: C) => D;
function curry(fn: Function): Function {
  return function curried(...args: any[]): any {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function (...args2: any[]) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
}

// Usage with type safety
const add = (a: number, b: number) => a + b;
const curriedAdd = curry(add);
const addTen = curriedAdd(10); // (b: number) => number
const result = addTen(5); // 15

Performance and Optimization

Optimize TypeScript compilation and runtime performance with these techniques.

Performance Best Practices

// ✅ Use const assertions for immutable data
const SUPPORTED_FORMATS = ['jpg', 'png', 'gif', 'webp'] as const;
type SupportedFormat = typeof SUPPORTED_FORMATS[number]; // 'jpg' | 'png' | 'gif' | 'webp'

const CONFIG = {
  api: {
    baseUrl: 'https://api.example.com',
    timeout: 5000
  },
  features: {
    darkMode: true,
    notifications: false
  }
} as const;

// ✅ Prefer union types over enums for better tree-shaking
type Status = 'loading' | 'success' | 'error' | 'idle';
const STATUS = {
  LOADING: 'loading' as const,
  SUCCESS: 'success' as const,
  ERROR: 'error' as const,
  IDLE: 'idle' as const
} as const;

// ✅ Use type-only imports to reduce bundle size
import type { User, UserRole } from './types/user';
import type { ApiResponse } from './types/api';

// Regular import for runtime values
import { validateUser, createUser } from './services/user';

// ✅ Lazy type evaluation for large types
type LazyUser<T = never> = T extends never 
  ? {
      id: string;
      profile: {
        name: string;
        email: string;
        preferences: {
          theme: 'light' | 'dark';
          language: string;
          notifications: boolean;
        };
      };
    }
  : T;

// Only evaluated when used
type ConcreteUser = LazyUser<true>;

// ✅ Efficient type guards
function isUserArray(value: unknown): value is User[] {
  return Array.isArray(value) && value.length > 0 && isUser(value[0]);
}

// ✅ Branded types for nominal typing
type UserId = string & { readonly brand: unique symbol };
type PostId = string & { readonly brand: unique symbol };

function createUserId(id: string): UserId {
  return id as UserId;
}

function createPostId(id: string): PostId {
  return id as PostId;
}

// These are now different types even though they're both strings
function getUser(id: UserId): Promise<User> {
  // Implementation
  return Promise.resolve({} as User);
}

function getPost(id: PostId): Promise<Post> {
  // Implementation  
  return Promise.resolve({} as Post);
}

// ❌ This would be a TypeScript error
// getUser(createPostId('123')); // Error: PostId is not assignable to UserId

// ✅ Template literal types for compile-time validation
type EventName = `user:${string}` | `post:${string}` | `system:${string}`;

function emitEvent(name: EventName, data: any): void {
  // Implementation
}

// Valid
emitEvent('user:login', { userId: '123' });
emitEvent('post:created', { postId: '456' });

// ❌ TypeScript error
// emitEvent('invalid:event', {}); // Error: not assignable to EventName

Performance Tips

  • • Use import type for type-only imports
  • • Prefer union types over enums for better tree-shaking
  • • Use const assertions to make literals more specific
  • • Leverage branded types for compile-time safety without runtime overhead