TypeScript Best Practices

// Leverages TypeScript type system for safer, more maintainable code

TypeScriptBest PracticesFrontend
Highly Rated
Community Verified

// detailed.guidelines

# TypeScript Best Practices

Leverage TypeScript's type system to catch errors early and improve code quality.

## Enable Strict Mode

```json
// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true
  }
}
```

## Use Explicit Types for Function Returns

```typescript
// Bad - inferred return type
function getUser(id: number) {
    return users.find(u => u.id === id);
}

// Good - explicit return type
function getUser(id: number): User | undefined {
    return users.find(u => u.id === id);
}
```

## Avoid any, Use unknown

```typescript
// Bad
function processData(data: any) {
    return data.value;  // No type safety
}

// 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 Type Guards

```typescript
interface Cat {
    meow(): void;
}

interface Dog {
    bark(): void;
}

// Type guard function
function isCat(pet: Cat | Dog): pet is Cat {
    return 'meow' in pet;
}

function makeSound(pet: Cat | Dog) {
    if (isCat(pet)) {
        pet.meow();  // TypeScript knows it's a Cat
    } else {
        pet.bark();  // TypeScript knows it's a Dog
    }
}
```

## Use Discriminated Unions

```typescript
type Success = {
    status: 'success';
    data: User;
};

type Error = {
    status: 'error';
    message: string;
};

type Result = Success | Error;

function handleResult(result: Result) {
    if (result.status === 'success') {
        console.log(result.data);  // TypeScript knows data exists
    } else {
        console.log(result.message);  // TypeScript knows message exists
    }
}
```

## Use Readonly for Immutability

```typescript
interface User {
    readonly id: number;
    readonly email: string;
    name: string;
}

const user: User = { id: 1, email: 'test@example.com', name: 'Alice' };
user.name = 'Bob';  // OK
user.id = 2;  // Error: Cannot assign to 'id'

// Readonly arrays
const numbers: readonly number[] = [1, 2, 3];
numbers.push(4);  // Error

// Or
const numbers: ReadonlyArray<number> = [1, 2, 3];
```

## Use Utility Types

```typescript
interface User {
    id: number;
    name: string;
    email: string;
    password: string;
}

// Partial - all properties optional
type PartialUser = Partial<User>;

// Pick - select specific properties
type UserPreview = Pick<User, 'id' | 'name'>;

// Omit - exclude specific properties
type UserWithoutPassword = Omit<User, 'password'>;

// Required - all properties required
type RequiredUser = Required<Partial<User>>;

// Record - create object type
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;
```

## Use never for Exhaustive Checks

```typescript
type Shape = Circle | Square | Triangle;

function getArea(shape: Shape): number {
    switch (shape.kind) {
        case 'circle':
            return Math.PI * shape.radius ** 2;
        case 'square':
            return shape.size ** 2;
        case 'triangle':
            return (shape.base * shape.height) / 2;
        default:
            // Ensures all cases are handled
            const _exhaustive: never = shape;
            throw new Error(`Unhandled shape: ${_exhaustive}`);
    }
}
```

## Use Generics for Reusability

```typescript
// Bad - repeated code
function wrapInArray(value: string): string[] {
    return [value];
}

function wrapNumberInArray(value: number): number[] {
    return [value];
}

// Good - generic
function wrapInArray<T>(value: T): T[] {
    return [value];
}

// Usage
const strings = wrapInArray('hello');  // string[]
const numbers = wrapInArray(42);  // number[]

// Generic constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}
```

## Use Const Assertions

```typescript
// Without const assertion
const config = {
    endpoint: 'https://api.example.com',
    timeout: 3000
};
// Type: { endpoint: string; timeout: number }

// With const assertion
const config = {
    endpoint: 'https://api.example.com',
    timeout: 3000
} as const;
// Type: { readonly endpoint: "https://api.example.com"; readonly timeout: 3000 }

// For arrays
const colors = ['red', 'green', 'blue'] as const;
type Color = typeof colors[number];  // 'red' | 'green' | 'blue'
```

## Avoid Type Assertions, Use Type Guards

```typescript
// Bad - type assertion
function processValue(value: unknown) {
    const str = value as string;
    return str.toUpperCase();  // Runtime error if not a string
}

// Good - type guard
function processValue(value: unknown) {
    if (typeof value !== 'string') {
        throw new Error('Value must be a string');
    }
    return value.toUpperCase();  // Safe
}
```

## Use Template Literal Types

```typescript
type Color = 'red' | 'green' | 'blue';
type Shade = 'light' | 'dark';

// Combine types
type ColorShade = `${Shade}-${Color}`;
// 'light-red' | 'light-green' | 'light-blue' | 
// 'dark-red' | 'dark-green' | 'dark-blue'
```

## Use Conditional Types

```typescript
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false

// Practical example
type NonNullable<T> = T extends null | undefined ? never : T;

type C = NonNullable<string | null>;  // string
```

## Use Index Signatures Carefully

```typescript
// Be specific when possible
interface StringMap {
    [key: string]: string;
}

// Better - known keys + index signature
interface Config {
    apiUrl: string;
    timeout: number;
    [key: string]: string | number;  // Additional properties
}

// Best - use Record when appropriate
type StringRecord = Record<string, string>;
```

## Use Enums or Const Objects

```typescript
// Enum (generates runtime code)
enum Status {
    Pending = 'PENDING',
    Active = 'ACTIVE',
    Inactive = 'INACTIVE'
}

// Const object (no runtime cost)
const Status = {
    Pending: 'PENDING',
    Active: 'ACTIVE',
    Inactive: 'INACTIVE'
} as const;

type StatusValue = typeof Status[keyof typeof Status];
```

## Use Non-Null Assertion Sparingly

```typescript
// Bad - hiding potential null
function getUser(id: number) {
    return users.find(u => u.id === id)!;  // Assumes always found
}

// Good - handle null case
function getUser(id: number): User {
    const user = users.find(u => u.id === id);
    if (!user) {
        throw new Error('User not found');
    }
    return user;
}
```

## Prefer Interface Over Type for Objects

```typescript
// Use interface for object shapes
interface User {
    id: number;
    name: string;
}

// Extend interface
interface AdminUser extends User {
    permissions: string[];
}

// Use type for unions, primitives, etc.
type ID = string | number;
type Status = 'active' | 'inactive';
```

TypeScript's type system prevents bugs before they reach runtime.