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.