Skip to main content
โšก Calmops

TypeScript Best Practices

TypeScript Best Practices

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';
    
  2. Not handling errors:

    // โŒ Bad
    const user = await getUser(id);
    
    // โœ… Good
    try {
      const user = await getUser(id);
    } catch (error) {
      // Handle error
    }
    
  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

Comments