Skip to main content

TypeScript Best Practices

Published: May 24, 2026 Updated: May 8, 2026 Larry Qu 8 min read

Best practices ensure maintainable, scalable TypeScript code. This article covers organization, patterns, and production guidelines.

Introduction

Best practices provide:

  • Code quality
  • Maintainability
  • Scalability
  • Performance
  • Team collaboration

Understanding best practices helps you:

  • Write better code
  • Avoid common pitfalls
  • Build scalable systems
  • Improve team productivity
  • Reduce technical debt

Type Safety

Strict Mode

// ✅ Good: Enable strict mode
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}

// ✅ Good: Explicit types
const user: { name: string; age: number } = {
  name: 'John',
  age: 30
};

// ❌ Bad: Implicit any
const user: any = { name: 'John', age: 30 };

Null and Undefined Handling

// ✅ Good: Handle null/undefined
function processUser(user: User | null | undefined): void {
  if (!user) {
    console.log('No user provided');
    return;
  }

  console.log(user.name);
}

// ✅ Good: Use optional chaining
const name = user?.name;
const email = user?.contact?.email;

// ✅ Good: Use nullish coalescing
const port = config.port ?? 3000;
const timeout = config.timeout ?? 5000;

// ❌ Bad: Assume non-null
function processUser(user: User): void {
  console.log(user.name); // May crash if user is null
}

Code Organization

Project Structure

// ✅ Good: Organized project structure
/*
src/
├── types/
│   ├── index.ts
│   ├── user.ts
│   └── api.ts
├── interfaces/
│   ├── index.ts
│   └── IRepository.ts
├── classes/
│   ├── User.ts
│   └── UserService.ts
├── utils/
│   ├── index.ts
│   ├── validators.ts
│   └── helpers.ts
├── services/
│   ├── index.ts
│   ├── UserService.ts
│   └── ApiService.ts
├── middleware/
│   └── auth.ts
├── config/
│   └── index.ts
└── index.ts
*/

// ✅ Good: Barrel exports
// src/types/index.ts
export type { User } from './user';
export type { Post } from './post';
export type { ApiResponse } from './api';

// Usage
import type { User, Post } from '@/types';

Naming Conventions

// ✅ Good: Clear naming
interface IUserRepository {
  getUser(id: number): Promise<User>;
}

class UserService {
  private userRepository: IUserRepository;

  async getUserById(id: number): Promise<User> {
    return this.userRepository.getUser(id);
  }
}

// ✅ Good: Consistent naming
const MAX_RETRIES = 3;
const DEFAULT_TIMEOUT = 5000;
const API_BASE_URL = 'https://api.example.com';

// ❌ Bad: Inconsistent naming
const maxRetries = 3;
const default_timeout = 5000;
const apiBaseUrl = 'https://api.example.com';

Error Handling

Custom Error Classes

// ✅ Good: Custom error classes
class ApplicationError extends Error {
  constructor(
    message: string,
    public statusCode: number = 500,
    public details?: any
  ) {
    super(message);
    this.name = 'ApplicationError';
  }
}

class ValidationError extends ApplicationError {
  constructor(message: string, details?: any) {
    super(message, 400, details);
    this.name = 'ValidationError';
  }
}

class NotFoundError extends ApplicationError {
  constructor(resource: string) {
    super(`${resource} not found`, 404);
    this.name = 'NotFoundError';
  }
}

// ✅ Good: Error handling
async function getUser(id: number): Promise<User> {
  if (!id || id <= 0) {
    throw new ValidationError('Invalid user ID');
  }

  const user = await database.findUser(id);

  if (!user) {
    throw new NotFoundError('User');
  }

  return user;
}

Error Handling Patterns

// ✅ Good: Try-catch with type guards
async function handleRequest(req: Request): Promise<Response> {
  try {
    const data = await processRequest(req);
    return { status: 200, data };
  } catch (error) {
    if (error instanceof ValidationError) {
      return { status: 400, error: error.message };
    }

    if (error instanceof NotFoundError) {
      return { status: 404, error: error.message };
    }

    if (error instanceof Error) {
      return { status: 500, error: error.message };
    }

    return { status: 500, error: 'Unknown error' };
  }
}

// ✅ Good: Result type pattern
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };

function createResult<T>(value: T): Result<T> {
  return { ok: true, value };
}

function createError<E>(error: E): Result<never, E> {
  return { ok: false, error };
}

async function getUser(id: number): Promise<Result<User>> {
  try {
    const user = await database.findUser(id);
    return createResult(user);
  } catch (error) {
    return createError(error);
  }
}

Testing

Type-Safe Testing

// ✅ Good: Type-safe tests
import { describe, it, expect } from '@jest/globals';

describe('UserService', () => {
  let userService: UserService;
  let mockRepository: jest.Mocked<IUserRepository>;

  beforeEach(() => {
    mockRepository = {
      getUser: jest.fn(),
      createUser: jest.fn(),
      updateUser: jest.fn(),
      deleteUser: jest.fn()
    };

    userService = new UserService(mockRepository);
  });

  it('should get user by id', async () => {
    const user: User = { id: 1, name: 'John', email: '[email protected]' };
    mockRepository.getUser.mockResolvedValue(user);

    const result = await userService.getUserById(1);

    expect(result).toEqual(user);
    expect(mockRepository.getUser).toHaveBeenCalledWith(1);
  });

  it('should throw NotFoundError when user not found', async () => {
    mockRepository.getUser.mockResolvedValue(null);

    await expect(userService.getUserById(999)).rejects.toThrow(NotFoundError);
  });
});

Performance

Optimization Patterns

// ✅ Good: Lazy loading
class DataService {
  private cache: Map<number, User> = new Map();

  async getUser(id: number): Promise<User> {
    if (this.cache.has(id)) {
      return this.cache.get(id)!;
    }

    const user = await this.fetchUser(id);
    this.cache.set(id, user);
    return user;
  }

  private async fetchUser(id: number): Promise<User> {
    // Fetch from API
    return { id, name: 'John', email: '[email protected]' };
  }
}

// ✅ Good: Memoization
function memoize<T extends (...args: any[]) => any>(fn: T): T {
  const cache = new Map();

  return ((...args: any[]) => {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      return cache.get(key);
    }

    const result = fn(...args);
    cache.set(key, result);
    return result;
  }) as T;
}

// ✅ Good: Batch operations
async function getUsersBatch(ids: number[]): Promise<User[]> {
  const uniqueIds = [...new Set(ids)];
  return Promise.all(uniqueIds.map(id => getUser(id)));
}

Dependency Injection

DI Pattern

// ✅ Good: Dependency injection
interface IUserRepository {
  getUser(id: number): Promise<User>;
}

interface IEmailService {
  sendEmail(to: string, subject: string, body: string): Promise<void>;
}

class UserService {
  constructor(
    private userRepository: IUserRepository,
    private emailService: IEmailService
  ) {}

  async createUser(data: CreateUserData): Promise<User> {
    const user = await this.userRepository.createUser(data);
    await this.emailService.sendEmail(
      user.email,
      'Welcome',
      'Welcome to our service'
    );
    return user;
  }
}

// ✅ Good: Container pattern
class Container {
  private services: Map<string, any> = new Map();

  register<T>(name: string, factory: () => T): void {
    this.services.set(name, factory);
  }

  get<T>(name: string): T {
    const factory = this.services.get(name);
    if (!factory) {
      throw new Error(`Service ${name} not found`);
    }
    return factory();
  }
}

const container = new Container();
container.register('userRepository', () => new UserRepository());
container.register('emailService', () => new EmailService());
container.register('userService', () => {
  return new UserService(
    container.get('userRepository'),
    container.get('emailService')
  );
});

Documentation

JSDoc Comments

// ✅ Good: Comprehensive JSDoc
/**
 * Fetches a user by ID
 * @param id - The user ID
 * @returns The user object
 * @throws {NotFoundError} If user not found
 * @example
 * const user = await getUser(1);
 */
async function getUser(id: number): Promise<User> {
  // Implementation
}

/**
 * Validates email format
 * @param email - Email to validate
 * @returns True if valid, false otherwise
 */
function isValidEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

/**
 * User service for managing users
 * @class
 */
class UserService {
  /**
   * Creates a new user
   * @param data - User data
   * @returns Created user
   */
  async createUser(data: CreateUserData): Promise<User> {
    // Implementation
  }
}

Production Patterns

Environment Configuration

// ✅ Good: Environment-based configuration
interface Config {
  nodeEnv: 'development' | 'production' | 'test';
  port: number;
  databaseUrl: string;
  apiKey: string;
  logLevel: 'debug' | 'info' | 'warn' | 'error';
}

function loadConfig(): Config {
  const nodeEnv = (process.env.NODE_ENV || 'development') as Config['nodeEnv'];

  const config: Config = {
    nodeEnv,
    port: parseInt(process.env.PORT || '3000', 10),
    databaseUrl: process.env.DATABASE_URL || 'postgresql://localhost/db',
    apiKey: process.env.API_KEY || '',
    logLevel: (process.env.LOG_LEVEL || 'info') as Config['logLevel']
  };

  if (!config.apiKey && nodeEnv === 'production') {
    throw new Error('API_KEY is required in production');
  }

  return config;
}

const config = loadConfig();

Logging

// ✅ Good: Structured logging
interface LogEntry {
  timestamp: string;
  level: 'debug' | 'info' | 'warn' | 'error';
  message: string;
  context?: Record<string, any>;
}

class Logger {
  private level: number;

  constructor(level: string = 'info') {
    this.level = { debug: 0, info: 1, warn: 2, error: 3 }[level] || 1;
  }

  private log(level: string, message: string, context?: any): void {
    const levelNum = { debug: 0, info: 1, warn: 2, error: 3 }[level] || 1;

    if (levelNum < this.level) return;

    const entry: LogEntry = {
      timestamp: new Date().toISOString(),
      level: level as any,
      message,
      context
    };

    console.log(JSON.stringify(entry));
  }

  debug(message: string, context?: any): void {
    this.log('debug', message, context);
  }

  info(message: string, context?: any): void {
    this.log('info', message, context);
  }

  warn(message: string, context?: any): void {
    this.log('warn', message, context);
  }

  error(message: string, context?: any): void {
    this.log('error', message, context);
  }
}

const logger = new Logger(config.logLevel);

Best Practices Summary

  1. Enable strict mode
  2. Use explicit types
  3. Handle null/undefined
  4. Organize code logically
  5. Use custom error classes
  6. Write type-safe tests
  7. Implement dependency injection
  8. Document with JSDoc
  9. Use environment configuration
  10. Implement structured logging

Common Mistakes

  1. Using any:
    // ❌ Bad
    const value: any = 'string';
    
    // ✅ Good
    const value: string = 'string';
    ```javascript
    
  2. Not handling errors:
    // ❌ Bad
    const user = await getUser(id);
    
    // ✅ Good
    try {
      const user = await getUser(id);
    } catch (error) {
      // Handle error
    }
    ```javascript
    
  3. Tight coupling:
    // ❌ Bad
    class UserService {
      private repository = new UserRepository();
    }
    
    // ✅ Good
    class UserService {
      constructor(private repository: IUserRepository) {}
    }
    

Summary

Best practices ensure quality. Key takeaways:

  • Enable strict mode
  • Use explicit types
  • Handle errors properly
  • Organize code logically
  • Implement dependency injection
  • Write type-safe tests
  • Document code
  • Use environment configuration
  • Implement logging
  • Avoid common pitfalls

Next Steps

  • Review all TypeScript articles
  • Practice TypeScript patterns
  • Build TypeScript projects
  • Contribute to TypeScript community
  • Stay updated with TypeScript releases

Resources

Comments

👍 Was this article helpful?