TypeScript Best Practices: A Comprehensive Guide to Type-Safe Development

🎯 Introduction

TypeScript has revolutionized JavaScript development by bringing static typing and advanced tooling to the ecosystem. However, leveraging TypeScript’s full potential requires understanding not just the syntax, but the principles and patterns that lead to maintainable, type-safe code.

This comprehensive guide explores TypeScript best practices across multiple dimensions:

  • Configuration & Setup: Optimal compiler settings and project structure
  • Type System Mastery: Leveraging TypeScript’s powerful type system effectively
  • Code Style & Syntax: Consistent, readable, and idiomatic TypeScript code
  • Design Patterns: Applying proven patterns in a type-safe manner
  • Advanced Techniques: Generics, utility types, and type transformations
  • Performance & Optimization: Writing efficient TypeScript code
  • Testing & Quality: Ensuring type safety extends to your test suite

πŸ’‘ Core Philosophy: “TypeScript is not just JavaScript with typesβ€”it’s a tool for designing robust APIs, catching bugs early, and enabling confident refactoring”

πŸ“‹ TypeScript Configuration Best Practices

πŸ”§ Essential tsconfig.json Settings

A properly configured tsconfig.json is the foundation of a type-safe TypeScript project.

 1{
 2  "compilerOptions": {
 3    // Strict Type Checking
 4    "strict": true,                           // Enable all strict type checking options
 5    "noImplicitAny": true,                   // Raise error on expressions with implied 'any'
 6    "strictNullChecks": true,                // Enable strict null checks
 7    "strictFunctionTypes": true,             // Enable strict checking of function types
 8    "strictBindCallApply": true,             // Enable strict bind/call/apply methods
 9    "strictPropertyInitialization": true,     // Ensure properties are initialized
10    "noImplicitThis": true,                  // Raise error on 'this' with implied 'any'
11    "alwaysStrict": true,                    // Parse in strict mode and emit "use strict"
12
13    // Additional Checks
14    "noUnusedLocals": true,                  // Report errors on unused locals
15    "noUnusedParameters": true,              // Report errors on unused parameters
16    "noImplicitReturns": true,               // Report error when not all paths return value
17    "noFallthroughCasesInSwitch": true,      // Report errors for fallthrough cases
18
19    // Module Resolution
20    "module": "ESNext",                      // Specify module code generation
21    "moduleResolution": "node",              // Use Node.js module resolution
22    "resolveJsonModule": true,               // Include modules imported with .json
23    "esModuleInterop": true,                 // Enable interop between CommonJS and ES Modules
24    "allowSyntheticDefaultImports": true,    // Allow default imports from modules
25
26    // Emit
27    "target": "ES2020",                      // Specify ECMAScript target version
28    "lib": ["ES2020", "DOM", "DOM.Iterable"], // Specify library files
29    "outDir": "./dist",                      // Redirect output structure to directory
30    "sourceMap": true,                       // Generate source maps
31    "declaration": true,                     // Generate .d.ts files
32    "declarationMap": true,                  // Generate sourcemap for .d.ts files
33    "removeComments": false,                 // Keep comments in output
34
35    // Interop Constraints
36    "isolatedModules": true,                 // Ensure each file can be transpiled
37    "allowJs": false,                        // Disallow JavaScript files
38    "checkJs": false,                        // Don't check JavaScript files
39
40    // Advanced
41    "skipLibCheck": true,                    // Skip type checking of declaration files
42    "forceConsistentCasingInFileNames": true, // Ensure consistent casing
43    "incremental": true,                     // Enable incremental compilation
44    "tsBuildInfoFile": "./dist/.tsbuildinfo" // Specify file for incremental info
45  },
46  "include": ["src/**/*"],
47  "exclude": ["node_modules", "dist", "**/*.spec.ts"]
48}

πŸ“Š Configuration Strategy by Project Type

graph TD
    A[Project Type] --> B[Library]
    A --> C[Application]
    A --> D[Monorepo]

    B --> B1[declaration: true]
    B --> B2[declarationMap: true]
    B --> B3[Remove comments: false]

    C --> C1[sourceMap: true]
    C --> C2[optimization flags]
    C --> C3[Runtime-specific lib]

    D --> D1[composite: true]
    D --> D2[Project references]
    D --> D3[Shared base config]

    style A fill:#ff6b6b
    style B fill:#4ecdc4
    style C fill:#feca57
    style D fill:#95e1d3

πŸ—οΈ Project Structure Best Practices

project-root/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ types/           # Shared type definitions
β”‚   β”‚   β”œβ”€β”€ index.ts
β”‚   β”‚   └── models.ts
β”‚   β”œβ”€β”€ utils/           # Utility functions
β”‚   β”‚   β”œβ”€β”€ validation.ts
β”‚   β”‚   └── formatting.ts
β”‚   β”œβ”€β”€ services/        # Business logic
β”‚   β”‚   └── api.service.ts
β”‚   β”œβ”€β”€ components/      # UI components (if applicable)
β”‚   └── index.ts         # Main entry point
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ unit/
β”‚   └── integration/
β”œβ”€β”€ tsconfig.json        # Base TypeScript config
β”œβ”€β”€ tsconfig.build.json  # Production build config
└── package.json

🎨 Type System Best Practices

βœ… Prefer Types Over Interfaces (When Appropriate)

 1// βœ… GOOD: Use type for unions, intersections, and primitives
 2type Status = 'pending' | 'success' | 'error';
 3type ID = string | number;
 4
 5type Point = {
 6  x: number;
 7  y: number;
 8};
 9
10// Intersection types
11type ColoredPoint = Point & {
12  color: string;
13};
14
15// βœ… GOOD: Use interface for object shapes that might be extended
16interface User {
17  id: string;
18  name: string;
19  email: string;
20}
21
22interface AdminUser extends User {
23  permissions: string[];
24}
25
26// ❌ AVOID: Using interface for union types (not possible)
27// interface Status = 'pending' | 'success' | 'error'; // Error!

Key Differences:

 1// Type aliases can represent any type
 2type StringOrNumber = string | number;
 3type Tuple = [string, number];
 4type Callback = (data: string) => void;
 5
 6// Interfaces are for object shapes and classes
 7interface Animal {
 8  name: string;
 9  makeSound(): void;
10}
11
12// Interfaces support declaration merging
13interface Window {
14  customProperty: string;
15}
16
17interface Window {
18  anotherProperty: number;
19}
20// Both declarations merge into one

πŸ”’ Embrace Strict Null Checks

 1// βœ… GOOD: Explicit handling of null/undefined
 2function getUserName(user: User | null): string {
 3  if (user === null) {
 4    return 'Guest';
 5  }
 6  return user.name;
 7}
 8
 9// βœ… GOOD: Optional chaining
10function getAddressCity(user?: User): string | undefined {
11  return user?.address?.city;
12}
13
14// βœ… GOOD: Nullish coalescing
15const displayName = user?.name ?? 'Anonymous';
16
17// ❌ AVOID: Non-null assertion (use sparingly)
18function processUser(user: User | null) {
19  console.log(user!.name); // Dangerous! Runtime error if null
20}
21
22// βœ… BETTER: Type guard
23function processUser(user: User | null) {
24  if (!user) {
25    throw new Error('User cannot be null');
26  }
27  console.log(user.name); // Safe: TypeScript knows user is not null
28}

🎯 Type Guards and Narrowing

 1// βœ… GOOD: User-defined type guards
 2interface Dog {
 3  type: 'dog';
 4  bark(): void;
 5}
 6
 7interface Cat {
 8  type: 'cat';
 9  meow(): void;
10}
11
12type Animal = Dog | Cat;
13
14// Type predicate
15function isDog(animal: Animal): animal is Dog {
16  return animal.type === 'dog';
17}
18
19function handleAnimal(animal: Animal) {
20  if (isDog(animal)) {
21    animal.bark(); // TypeScript knows it's a Dog
22  } else {
23    animal.meow(); // TypeScript knows it's a Cat
24  }
25}
26
27// βœ… GOOD: Discriminated unions
28type Shape =
29  | { kind: 'circle'; radius: number }
30  | { kind: 'rectangle'; width: number; height: number }
31  | { kind: 'square'; size: number };
32
33function getArea(shape: Shape): number {
34  switch (shape.kind) {
35    case 'circle':
36      return Math.PI * shape.radius ** 2;
37    case 'rectangle':
38      return shape.width * shape.height;
39    case 'square':
40      return shape.size ** 2;
41    default:
42      // Exhaustiveness check
43      const _exhaustive: never = shape;
44      throw new Error(`Unhandled shape: ${_exhaustive}`);
45  }
46}

🧩 Generics Best Practices

 1// βœ… GOOD: Generic function with constraints
 2function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
 3  return obj[key];
 4}
 5
 6const user = { name: 'John', age: 30 };
 7const name = getProperty(user, 'name'); // Type: string
 8const age = getProperty(user, 'age');   // Type: number
 9
10// βœ… GOOD: Generic interface with defaults
11interface ApiResponse<T = unknown> {
12  data: T;
13  status: number;
14  message: string;
15}
16
17const userResponse: ApiResponse<User> = {
18  data: { id: '1', name: 'John', email: 'john@example.com' },
19  status: 200,
20  message: 'Success'
21};
22
23// βœ… GOOD: Conditional types
24type Unwrap<T> = T extends Promise<infer U> ? U : T;
25
26type A = Unwrap<Promise<string>>;  // string
27type B = Unwrap<number>;           // number
28
29// βœ… GOOD: Mapped types
30type Readonly<T> = {
31  readonly [P in keyof T]: T[P];
32};
33
34type Partial<T> = {
35  [P in keyof T]?: T[P];
36};
37
38// ❌ AVOID: Overly complex generics
39type ComplexGeneric<T extends Record<string, unknown>, K extends keyof T, V extends T[K]> = {
40  [P in K]: V;
41};
42
43// βœ… BETTER: Break it down
44type SimpleValue<T, K extends keyof T> = T[K];

πŸ’… Code Style and Syntax Best Practices

πŸ“ Naming Conventions

 1// βœ… GOOD: Clear, descriptive names
 2// Interfaces and Types: PascalCase
 3interface UserProfile { }
 4type RequestStatus = 'pending' | 'success' | 'error';
 5
 6// Classes: PascalCase
 7class UserService { }
 8
 9// Functions and variables: camelCase
10function calculateTotal(items: Item[]): number { }
11const userName = 'John';
12
13// Constants: UPPER_SNAKE_CASE
14const MAX_RETRY_ATTEMPTS = 3;
15const API_BASE_URL = 'https://api.example.com';
16
17// Enums: PascalCase for enum, UPPER_CASE for members
18enum HttpStatus {
19  OK = 200,
20  NOT_FOUND = 404,
21  INTERNAL_SERVER_ERROR = 500
22}
23
24// Private members: prefix with underscore (optional)
25class User {
26  private _password: string;
27
28  constructor(password: string) {
29    this._password = password;
30  }
31}
32
33// Boolean variables: use is/has/should prefix
34const isLoading = true;
35const hasPermission = false;
36const shouldRetry = true;

🎯 Function Best Practices

 1// βœ… GOOD: Explicit return types
 2function calculatePrice(quantity: number, unitPrice: number): number {
 3  return quantity * unitPrice;
 4}
 5
 6// βœ… GOOD: Use function overloads for different signatures
 7function createElement(tag: 'div'): HTMLDivElement;
 8function createElement(tag: 'span'): HTMLSpanElement;
 9function createElement(tag: string): HTMLElement;
10function createElement(tag: string): HTMLElement {
11  return document.createElement(tag);
12}
13
14// βœ… GOOD: Optional parameters come last
15function greet(name: string, greeting?: string): string {
16  return `${greeting ?? 'Hello'}, ${name}!`;
17}
18
19// βœ… GOOD: Use destructuring with types
20interface UserOptions {
21  name: string;
22  age?: number;
23  email?: string;
24}
25
26function createUser({ name, age = 18, email }: UserOptions): User {
27  return { name, age, email: email ?? '' };
28}
29
30// βœ… GOOD: Arrow functions for callbacks
31const numbers = [1, 2, 3, 4, 5];
32const doubled = numbers.map((n) => n * 2);
33const evenNumbers = numbers.filter((n) => n % 2 === 0);
34
35// ❌ AVOID: Implicit any parameters
36function process(data) { // Error: Parameter 'data' implicitly has an 'any' type
37  console.log(data);
38}
39
40// βœ… GOOD: Typed parameters
41function process(data: unknown): void {
42  console.log(data);
43}

🏷️ Enum vs Union Types

 1// βœ… GOOD: Use const enum for compile-time constants
 2const enum Direction {
 3  Up,
 4  Down,
 5  Left,
 6  Right
 7}
 8
 9const direction: Direction = Direction.Up;
10
11// βœ… GOOD: Use string literal unions for flexibility
12type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
13
14function request(url: string, method: HttpMethod): Promise<Response> {
15  return fetch(url, { method });
16}
17
18// βœ… GOOD: Use as const for readonly literal types
19const ROUTES = {
20  HOME: '/',
21  ABOUT: '/about',
22  CONTACT: '/contact'
23} as const;
24
25type Route = typeof ROUTES[keyof typeof ROUTES]; // '/' | '/about' | '/contact'
26
27// ❌ AVOID: Regular enums if string unions suffice
28enum Status {
29  Pending = 'PENDING',
30  Success = 'SUCCESS',
31  Error = 'ERROR'
32}
33
34// βœ… BETTER: String union
35type Status = 'PENDING' | 'SUCCESS' | 'ERROR';

πŸ›οΈ Design Patterns in TypeScript

🏭 Singleton Pattern (Type-Safe)

 1// βœ… GOOD: Type-safe singleton with private constructor
 2class DatabaseConnection {
 3  private static instance: DatabaseConnection;
 4  private connectionString: string;
 5
 6  private constructor(connectionString: string) {
 7    this.connectionString = connectionString;
 8  }
 9
10  public static getInstance(connectionString?: string): DatabaseConnection {
11    if (!DatabaseConnection.instance) {
12      if (!connectionString) {
13        throw new Error('Connection string required for first initialization');
14      }
15      DatabaseConnection.instance = new DatabaseConnection(connectionString);
16    }
17    return DatabaseConnection.instance;
18  }
19
20  public query<T>(sql: string): Promise<T[]> {
21    // Implementation
22    return Promise.resolve([]);
23  }
24}
25
26// Usage
27const db = DatabaseConnection.getInstance('postgres://localhost:5432/mydb');
28const users = await db.query<User>('SELECT * FROM users');

πŸ—οΈ Factory Pattern with Generics

 1// βœ… GOOD: Generic factory pattern
 2interface Product {
 3  id: string;
 4  name: string;
 5  price: number;
 6}
 7
 8interface Service {
 9  id: string;
10  name: string;
11  duration: number;
12}
13
14interface Factory<T> {
15  create(data: Omit<T, 'id'>): T;
16}
17
18class ProductFactory implements Factory<Product> {
19  private idCounter = 0;
20
21  create(data: Omit<Product, 'id'>): Product {
22    return {
23      id: `product-${++this.idCounter}`,
24      ...data
25    };
26  }
27}
28
29class ServiceFactory implements Factory<Service> {
30  private idCounter = 0;
31
32  create(data: Omit<Service, 'id'>): Service {
33    return {
34      id: `service-${++this.idCounter}`,
35      ...data
36    };
37  }
38}
39
40// Usage
41const productFactory = new ProductFactory();
42const product = productFactory.create({ name: 'Laptop', price: 1200 });
43
44const serviceFactory = new ServiceFactory();
45const service = serviceFactory.create({ name: 'Consultation', duration: 60 });

🎭 Strategy Pattern with Type Safety

 1// βœ… GOOD: Type-safe strategy pattern
 2interface PaymentStrategy {
 3  pay(amount: number): Promise<PaymentResult>;
 4}
 5
 6interface PaymentResult {
 7  success: boolean;
 8  transactionId?: string;
 9  error?: string;
10}
11
12class CreditCardPayment implements PaymentStrategy {
13  constructor(
14    private cardNumber: string,
15    private cvv: string,
16    private expiryDate: string
17  ) {}
18
19  async pay(amount: number): Promise<PaymentResult> {
20    // Implementation
21    return {
22      success: true,
23      transactionId: `cc-${Date.now()}`
24    };
25  }
26}
27
28class PayPalPayment implements PaymentStrategy {
29  constructor(private email: string, private password: string) {}
30
31  async pay(amount: number): Promise<PaymentResult> {
32    // Implementation
33    return {
34      success: true,
35      transactionId: `pp-${Date.now()}`
36    };
37  }
38}
39
40class PaymentProcessor {
41  constructor(private strategy: PaymentStrategy) {}
42
43  setStrategy(strategy: PaymentStrategy): void {
44    this.strategy = strategy;
45  }
46
47  async processPayment(amount: number): Promise<PaymentResult> {
48    return this.strategy.pay(amount);
49  }
50}
51
52// Usage
53const processor = new PaymentProcessor(
54  new CreditCardPayment('1234-5678-9012-3456', '123', '12/25')
55);
56await processor.processPayment(100);
57
58processor.setStrategy(new PayPalPayment('user@example.com', 'password'));
59await processor.processPayment(50);

πŸ” Observer Pattern with Strong Typing

 1// βœ… GOOD: Type-safe observer pattern
 2type Observer<T> = (data: T) => void;
 3
 4class Observable<T> {
 5  private observers: Set<Observer<T>> = new Set();
 6
 7  subscribe(observer: Observer<T>): () => void {
 8    this.observers.add(observer);
 9
10    // Return unsubscribe function
11    return () => {
12      this.observers.delete(observer);
13    };
14  }
15
16  notify(data: T): void {
17    this.observers.forEach((observer) => observer(data));
18  }
19}
20
21// Usage with specific types
22interface UserEvent {
23  type: 'login' | 'logout';
24  userId: string;
25  timestamp: number;
26}
27
28const userEvents = new Observable<UserEvent>();
29
30const unsubscribe = userEvents.subscribe((event) => {
31  console.log(`User ${event.userId} performed ${event.type}`);
32});
33
34userEvents.notify({
35  type: 'login',
36  userId: 'user-123',
37  timestamp: Date.now()
38});
39
40unsubscribe(); // Clean up

πŸš€ Advanced TypeScript Techniques

πŸ”§ Utility Types Mastery

 1// βœ… GOOD: Leverage built-in utility types
 2interface User {
 3  id: string;
 4  name: string;
 5  email: string;
 6  password: string;
 7  role: 'admin' | 'user';
 8}
 9
10// Partial: All properties optional
11type UserUpdate = Partial<User>;
12
13// Pick: Select specific properties
14type UserPublic = Pick<User, 'id' | 'name' | 'email'>;
15
16// Omit: Exclude specific properties
17type UserWithoutPassword = Omit<User, 'password'>;
18
19// Required: All properties required
20type UserRequired = Required<Partial<User>>;
21
22// Readonly: All properties readonly
23type UserImmutable = Readonly<User>;
24
25// Record: Create object type with specific keys
26type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;
27
28// βœ… GOOD: Custom utility types
29type DeepPartial<T> = {
30  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
31};
32
33type DeepReadonly<T> = {
34  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
35};
36
37// Make specific keys optional
38type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
39
40type UserOptionalEmail = OptionalKeys<User, 'email'>;
41
42// Make specific keys required
43type RequiredKeys<T, K extends keyof T> = T & Required<Pick<T, K>>;

🎨 Template Literal Types

 1// βœ… GOOD: Template literal types for type-safe strings
 2type EventName = 'click' | 'focus' | 'blur';
 3type ElementId = 'button' | 'input' | 'form';
 4
 5type ElementEvent = `${ElementId}:${EventName}`;
 6// Result: 'button:click' | 'button:focus' | 'button:blur' | 'input:click' | ...
 7
 8// βœ… GOOD: Type-safe API routes
 9type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
10type ApiVersion = 'v1' | 'v2';
11type Resource = 'users' | 'posts' | 'comments';
12
13type ApiRoute = `/${ApiVersion}/${Resource}`;
14type ApiEndpoint = `${HttpMethod} ${ApiRoute}`;
15
16const endpoint: ApiEndpoint = 'GET /v1/users'; // Type-safe!
17
18// βœ… GOOD: CSS-in-JS type safety
19type CSSUnit = 'px' | 'em' | 'rem' | '%';
20type Size = `${number}${CSSUnit}`;
21
22interface Style {
23  width: Size;
24  height: Size;
25  margin: Size;
26}
27
28const style: Style = {
29  width: '100px',
30  height: '50rem',
31  margin: '10%'
32};

πŸ” Branded Types for Type Safety

 1// βœ… GOOD: Branded types prevent mixing incompatible values
 2type Brand<K, T> = K & { __brand: T };
 3
 4type UserId = Brand<string, 'UserId'>;
 5type PostId = Brand<string, 'PostId'>;
 6type Email = Brand<string, 'Email'>;
 7
 8// Factory functions for creating branded types
 9function createUserId(id: string): UserId {
10  return id as UserId;
11}
12
13function createPostId(id: string): PostId {
14  return id as PostId;
15}
16
17function createEmail(email: string): Email {
18  if (!email.includes('@')) {
19    throw new Error('Invalid email format');
20  }
21  return email as Email;
22}
23
24// Type-safe functions
25function getUserById(userId: UserId): User | null {
26  // Implementation
27  return null;
28}
29
30function getPostById(postId: PostId): Post | null {
31  // Implementation
32  return null;
33}
34
35// Usage
36const userId = createUserId('user-123');
37const postId = createPostId('post-456');
38
39getUserById(userId); // βœ… OK
40getUserById(postId); // ❌ Error: Type 'PostId' is not assignable to type 'UserId'

πŸ§ͺ Conditional Types and Inference

 1// βœ… GOOD: Extract return type
 2type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
 3
 4function getUser(): User {
 5  return { id: '1', name: 'John', email: 'john@example.com' };
 6}
 7
 8type UserType = ReturnType<typeof getUser>; // User
 9
10// βœ… GOOD: Extract array element type
11type ArrayElement<T> = T extends (infer E)[] ? E : never;
12
13type StringArray = string[];
14type Element = ArrayElement<StringArray>; // string
15
16// βœ… GOOD: Flatten promise types
17type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
18
19type NestedPromise = Promise<Promise<User>>;
20type FlattenedUser = Awaited<NestedPromise>; // User
21
22// βœ… GOOD: Function parameter types
23type Parameters<T> = T extends (...args: infer P) => any ? P : never;
24
25function createUser(name: string, age: number, email: string): User {
26  return { id: '1', name, email };
27}
28
29type CreateUserParams = Parameters<typeof createUser>; // [string, number, string]

πŸ§ͺ Testing Best Practices

βœ… Type-Safe Test Setup

 1// βœ… GOOD: Type-safe test data factories
 2interface User {
 3  id: string;
 4  name: string;
 5  email: string;
 6  age: number;
 7}
 8
 9function createMockUser(overrides?: Partial<User>): User {
10  return {
11    id: 'test-id',
12    name: 'Test User',
13    email: 'test@example.com',
14    age: 25,
15    ...overrides
16  };
17}
18
19// βœ… GOOD: Type-safe mocks with Jest
20import { jest } from '@jest/globals';
21
22interface UserService {
23  getUser(id: string): Promise<User>;
24  updateUser(id: string, data: Partial<User>): Promise<User>;
25}
26
27const mockUserService: jest.Mocked<UserService> = {
28  getUser: jest.fn(),
29  updateUser: jest.fn()
30};
31
32// Type-safe mock implementation
33mockUserService.getUser.mockResolvedValue(createMockUser());
34
35// βœ… GOOD: Type assertions in tests
36describe('User Service', () => {
37  it('should return user data', async () => {
38    const user = await userService.getUser('123');
39
40    // Type-safe assertions
41    expect(user).toMatchObject<Partial<User>>({
42      id: '123',
43      name: expect.any(String),
44      email: expect.any(String)
45    });
46  });
47});

⚑ Performance and Optimization

🎯 Type-Level Performance

 1// ❌ AVOID: Deep type recursion
 2type DeepNested<T, N extends number> = N extends 0
 3  ? T
 4  : DeepNested<T[], Subtract<N, 1>>;
 5
 6// βœ… GOOD: Limit recursion depth
 7type DeepPartial<T> = T extends object
 8  ? { [P in keyof T]?: DeepPartial<T[P]> }
 9  : T;
10
11// ❌ AVOID: Excessive union types
12type AllCombinations =
13  | Type1 | Type2 | Type3 | Type4 | Type5
14  | Type6 | Type7 | Type8 | Type9 | Type10; // ... 100 more
15
16// βœ… GOOD: Use discriminated unions
17type Action =
18  | { type: 'INCREMENT'; payload: number }
19  | { type: 'DECREMENT'; payload: number }
20  | { type: 'RESET' };

πŸš€ Runtime Performance

 1// βœ… GOOD: Use const assertions to avoid runtime overhead
 2const config = {
 3  apiUrl: 'https://api.example.com',
 4  timeout: 5000,
 5  retries: 3
 6} as const;
 7
 8// Type is: { readonly apiUrl: "https://api.example.com"; readonly timeout: 5000; readonly retries: 3 }
 9
10// βœ… GOOD: Avoid unnecessary type casting
11function processData(data: unknown): string {
12  // Type guard instead of casting
13  if (typeof data === 'string') {
14    return data.toUpperCase();
15  }
16  return String(data);
17}
18
19// ❌ AVOID: Excessive type assertions
20function process(value: unknown): string {
21  return (value as any).toString(); // Loses type safety
22}

🚨 Common Pitfalls and How to Avoid Them

⚠️ Pitfall 1: Type Assertions vs Type Guards

 1// ❌ BAD: Unsafe type assertion
 2function processUser(data: unknown) {
 3  const user = data as User; // No runtime check!
 4  console.log(user.name.toUpperCase()); // May crash
 5}
 6
 7// βœ… GOOD: Type guard with validation
 8function isUser(data: unknown): data is User {
 9  return (
10    typeof data === 'object' &&
11    data !== null &&
12    'id' in data &&
13    'name' in data &&
14    'email' in data
15  );
16}
17
18function processUser(data: unknown) {
19  if (!isUser(data)) {
20    throw new Error('Invalid user data');
21  }
22  console.log(data.name.toUpperCase()); // Safe!
23}

⚠️ Pitfall 2: Any vs Unknown

 1// ❌ BAD: Using 'any' loses type safety
 2function processData(data: any): string {
 3  return data.toString(); // No type checking
 4}
 5
 6// βœ… GOOD: Use 'unknown' and narrow the type
 7function processData(data: unknown): string {
 8  if (typeof data === 'string') {
 9    return data;
10  }
11  if (typeof data === 'number') {
12    return String(data);
13  }
14  throw new Error('Unsupported data type');
15}

⚠️ Pitfall 3: Optional vs Undefined

 1// ❌ CONFUSING: Mixing optional and undefined
 2interface User {
 3  name?: string | undefined;  // Redundant
 4  email?: string;             // Better
 5}
 6
 7// βœ… GOOD: Be explicit about intent
 8interface Config {
 9  timeout?: number;        // Can be omitted
10  retries: number | undefined; // Must be provided, can be undefined
11}
12
13const config1: Config = { retries: undefined }; // βœ… OK
14const config2: Config = { retries: 3 }; // βœ… OK
15const config3: Config = {}; // ❌ Error: retries is required

⚠️ Pitfall 4: Array Methods and Type Narrowing

 1// ❌ BAD: Filter doesn't narrow types automatically
 2const values: (string | null)[] = ['a', null, 'b', null];
 3const strings = values.filter(v => v !== null);
 4// Type is still (string | null)[]
 5
 6// βœ… GOOD: Use type predicate
 7function isNotNull<T>(value: T | null): value is T {
 8  return value !== null;
 9}
10
11const strings = values.filter(isNotNull); // Type is string[]

πŸ“š Summary and Quick Reference

🎯 Key Principles

  1. Enable Strict Mode: Always use "strict": true in tsconfig.json
  2. Prefer Type Safety: Choose unknown over any, use type guards instead of assertions
  3. Leverage the Type System: Use discriminated unions, branded types, and utility types
  4. Be Explicit: Add return types to functions, use const assertions
  5. Design for Maintainability: Use clear naming, consistent patterns, and proper structure

πŸ› οΈ Essential Commands

 1# Initialize TypeScript project
 2npm init -y
 3npm install -D typescript @types/node
 4
 5# Create tsconfig.json
 6npx tsc --init --strict
 7
 8# Type check without emitting
 9npx tsc --noEmit
10
11# Watch mode
12npx tsc --watch
13
14# Build project
15npx tsc
16
17# Run TypeScript directly (development)
18npx tsx src/index.ts
 1{
 2  "devDependencies": {
 3    "typescript": "^5.3.0",
 4    "@typescript-eslint/eslint-plugin": "^6.0.0",
 5    "@typescript-eslint/parser": "^6.0.0",
 6    "eslint": "^8.0.0",
 7    "prettier": "^3.0.0",
 8    "ts-node": "^10.0.0",
 9    "tsx": "^4.0.0",
10    "@types/node": "^20.0.0",
11    "jest": "^29.0.0",
12    "@types/jest": "^29.0.0",
13    "ts-jest": "^29.0.0"
14  }
15}

🎨 ESLint Configuration for TypeScript

 1{
 2  "parser": "@typescript-eslint/parser",
 3  "extends": [
 4    "eslint:recommended",
 5    "plugin:@typescript-eslint/recommended",
 6    "plugin:@typescript-eslint/recommended-requiring-type-checking"
 7  ],
 8  "parserOptions": {
 9    "project": "./tsconfig.json"
10  },
11  "rules": {
12    "@typescript-eslint/no-explicit-any": "error",
13    "@typescript-eslint/explicit-function-return-type": "warn",
14    "@typescript-eslint/no-unused-vars": "error",
15    "@typescript-eslint/no-non-null-assertion": "warn"
16  }
17}

πŸŽ“ Further Learning Resources

🎯 Conclusion

TypeScript is a powerful tool that, when used correctly, dramatically improves code quality, maintainability, and developer experience. By following these best practices, you’ll write type-safe code that catches bugs early, enables confident refactoring, and scales with your application’s growth.

Remember: TypeScript is not just about adding typesβ€”it’s about designing better APIs, expressing intent clearly, and leveraging the compiler as a powerful ally in your development workflow.


Related Posts:

Tags: #TypeScript #JavaScript #TypeSafety #DesignPatterns #BestPractices #SoftwareEngineering #WebDevelopment #FrontendDevelopment