</>StackKit
</>StackKit

Developer tutorials & guides

TypeScript code with type annotations
TypeScript

TypeScript Best Practices Every Developer Should Follow in 2025

Write safer, cleaner TypeScript with these battle-tested best practices covering types, generics, strict mode, utility types, and common mistakes to avoid.

L
Leila Hassan
April 18, 20258 min read
#typescript#javascript#best-practices#frontend

Why TypeScript Best Practices Matter

TypeScript can be used minimally — sprinkling any everywhere and calling it done — or it can be used as a powerful correctness tool. The difference between the two is your team's knowledge of its best practices. This guide covers the patterns that make TypeScript genuinely useful.


1. Enable Strict Mode

Always start with strict: true in your tsconfig.json:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true
  }
}

This enables strictNullChecks, noImplicitAny, strictFunctionTypes, and more. If you start a project without strict mode, adding it later is painful.


2. Avoid any — Use unknown Instead

any completely disables type checking. unknown forces you to narrow the type before using it:

// Bad
function processInput(input: any) {
  return input.toUpperCase(); // No error, but crashes at runtime if input is a number
}

// Good
function processInput(input: unknown) {
  if (typeof input === 'string') {
    return input.toUpperCase(); // TypeScript is now satisfied
  }
  throw new Error('Expected string');
}

3. Use Discriminated Unions for State

Instead of booleans and optional fields, model state explicitly:

// Messy
interface RequestState {
  loading: boolean;
  data?: User;
  error?: string;
}

// Clean — only valid combinations are representable
type RequestState =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: User }
  | { status: 'error'; message: string };

4. Use Utility Types

TypeScript ships with powerful utility types. Use them instead of duplicating type definitions:

interface User {
  id: string;
  name: string;
  email: string;
  password: string;
}

type PublicUser = Omit<User, 'password'>;
type CreateUserDTO = Pick<User, 'name' | 'email' | 'password'>;
type PartialUser = Partial<User>;       // all fields optional
type ReadonlyUser = Readonly<User>;     // all fields readonly

5. Const Assertions for Literal Types

const ROLES = ['admin', 'editor', 'viewer'] as const;
type Role = typeof ROLES[number]; // "admin" | "editor" | "viewer"

const config = {
  endpoint: '/api/v2',
  timeout: 5000,
} as const;

6. Prefer Interfaces for Objects, Types for Unions

// Interface for object shapes (supports declaration merging)
interface User {
  id: string;
  name: string;
}

// Type alias for unions, tuples, mapped types
type StringOrNumber = string | number;
type Pair<T> = [T, T];

7. Use Generics Properly

// Too loose
function first(arr: any[]): any {
  return arr[0];
}

// Type-safe and reusable
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const name = first(['Alice', 'Bob']); // inferred as string | undefined

8. Type Your API Responses

Don't leave API data untyped. Define response shapes explicitly:

interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

async function fetchUser(id: string): Promise<ApiResponse<User>> {
  const res = await fetch(`/api/users/${id}`);
  return res.json() as Promise<ApiResponse<User>>;
}

9. Never Use Enums — Use Const Enums or Union Types

Regular enums compile to verbose JavaScript. Prefer:

// Instead of enum
const Direction = {
  UP: 'UP',
  DOWN: 'DOWN',
  LEFT: 'LEFT',
  RIGHT: 'RIGHT',
} as const;
type Direction = typeof Direction[keyof typeof Direction];

10. Use satisfies for Validation Without Widening

const config = {
  port: 3000,
  host: 'localhost',
} satisfies Record<string, string | number>;

// config.port is still inferred as 3000, not number | string

Conclusion

The goal of TypeScript isn't just to add types — it's to make impossible states unrepresentable. When you use discriminated unions, strict mode, generics, and utility types properly, TypeScript catches entire categories of bugs before they reach production. These practices make that possible.

#typescript#javascript#best-practices#frontend