TypeScript Best Practices for Clean Code

March 10, 2024 (1y ago)

TypeScript Best Practices for Clean Code

TypeScript helps catch errors early and improves code quality. Here are the best practices to write cleaner, safer code.

Strict Configuration

tsconfig.json

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "exactOptionalPropertyTypes": true,
    "noUncheckedIndexedAccess": true
  }
}

Type Definitions

Use Interface for Objects

// Good - Use interface for object shapes
interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}
 
// Use type for unions, intersections, primitives
type Status = 'pending' | 'active' | 'inactive';
type ID = string | number;

Extend Interfaces

interface BaseEntity {
  id: string;
  createdAt: Date;
  updatedAt: Date;
}
 
interface User extends BaseEntity {
  name: string;
  email: string;
}
 
interface Post extends BaseEntity {
  title: string;
  content: string;
  authorId: string;
}

Avoid any

Use unknown Instead

// Bad
function processData(data: any) {
  return data.value;
}
 
// Good
function processData(data: unknown) {
  if (typeof data === 'object' && data !== null && 'value' in data) {
    return (data as { value: string }).value;
  }
  throw new Error('Invalid data');
}

Use Generics

// Generic function
function getFirst<T>(arr: T[]): T | undefined {
  return arr[0];
}
 
// Generic interface
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}
 
// Usage
const response: ApiResponse<User[]> = await fetchUsers();

Type Guards

Custom Type Guard

interface Dog {
  kind: 'dog';
  bark(): void;
}
 
interface Cat {
  kind: 'cat';
  meow(): void;
}
 
type Animal = Dog | Cat;
 
function isDog(animal: Animal): animal is Dog {
  return animal.kind === 'dog';
}
 
function handleAnimal(animal: Animal) {
  if (isDog(animal)) {
    animal.bark(); // TypeScript knows it's a Dog
  } else {
    animal.meow(); // TypeScript knows it's a Cat
  }
}

Utility Types

interface User {
  id: string;
  name: string;
  email: string;
  password: string;
}
 
// Partial - All properties optional
type UpdateUserDto = Partial<User>;
 
// Pick - Select specific properties
type UserPublic = Pick<User, 'id' | 'name' | 'email'>;
 
// Omit - Exclude properties
type UserWithoutPassword = Omit<User, 'password'>;
 
// Required - All properties required
type RequiredUser = Required<User>;
 
// Readonly - All properties readonly
type ReadonlyUser = Readonly<User>;
 
// Record - Key-value mapping
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;

Discriminated Unions

type Result<T> =
  | { success: true; data: T }
  | { success: false; error: string };
 
function handleResult<T>(result: Result<T>) {
  if (result.success) {
    console.log(result.data); // TypeScript knows data exists
  } else {
    console.error(result.error); // TypeScript knows error exists
  }
}

Const Assertions

// Without as const - type is string[]
const colors = ['red', 'green', 'blue'];
 
// With as const - type is readonly ['red', 'green', 'blue']
const colorsConst = ['red', 'green', 'blue'] as const;
 
// Object with as const
const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
} as const;

Enums vs Union Types

// Prefer union types for simple cases
type Direction = 'north' | 'south' | 'east' | 'west';
 
// Use const enum for performance (inlined at compile time)
const enum HttpStatus {
  OK = 200,
  NotFound = 404,
  ServerError = 500,
}
 
// Or use object with as const
const HTTP_STATUS = {
  OK: 200,
  NOT_FOUND: 404,
  SERVER_ERROR: 500,
} as const;
 
type HttpStatusCode = (typeof HTTP_STATUS)[keyof typeof HTTP_STATUS];

Function Overloads

function formatDate(date: Date): string;
function formatDate(date: string): Date;
function formatDate(date: Date | string): string | Date {
  if (date instanceof Date) {
    return date.toISOString();
  }
  return new Date(date);
}

Nullish Coalescing & Optional Chaining

interface Config {
  api?: {
    url?: string;
    timeout?: number;
  };
}
 
function getApiUrl(config: Config): string {
  // Optional chaining with nullish coalescing
  return config.api?.url ?? 'https://default.api.com';
}

Template Literal Types

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = '/users' | '/posts' | '/comments';
 
type ApiRoute = `${HttpMethod} ${Endpoint}`;
// "GET /users" | "GET /posts" | "POST /users" | etc.

Following these TypeScript best practices will help you write safer, more maintainable code that catches bugs at compile time rather than runtime.