Frontend

TypeScript Best Practices

Modern TypeScript patterns and type-safe coding practices for robust applications.

Cursor Add to Cursor
Cursor Rule (.mdc)

Copy this complete rule and save it as a .mdc file in your .cursor/rules directory

# TypeScript Best Practices

## Type Inference

### Let TypeScript Infer When Possible
```typescript
// Unnecessary explicit type
const name: string = 'John';

// Let TypeScript infer
const name = 'John'; // inferred as string

// Explicit types are needed for function parameters
function greet(name: string): string {
  return `Hello, ${name}`;
}
```

### Use `const` Assertions
```typescript
// Without const assertion - type is string[]
const roles = ['admin', 'user', 'guest'];

// With const assertion - type is readonly ['admin', 'user', 'guest']
const roles = ['admin', 'user', 'guest'] as const;

// Useful for creating union types
type Role = typeof roles[number]; // 'admin' | 'user' | 'guest'
```

## Interfaces vs Types

### Use Interfaces for Objects
```typescript
// Interfaces are extendable and show better error messages
interface User {
  id: string;
  name: string;
  email: string;
}

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

### Use Types for Unions and Utilities
```typescript
type Status = 'pending' | 'active' | 'archived';
type Nullable<T> = T | null;
type UserOrAdmin = User | AdminUser;
```

## Utility Types

### Common Patterns
```typescript
interface User {
  id: string;
  name: string;
  email: string;
  password: string;
}

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

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

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

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

// Readonly - immutable properties
type ImmutableUser = Readonly<User>;

// Record - key-value mapping
type UserRoles = Record<string, Role>;
```

## Generics

### Basic Generic Functions
```typescript
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const num = first([1, 2, 3]); // number
const str = first(['a', 'b']); // string
```

### Constrained Generics
```typescript
interface HasId {
  id: string;
}

function findById<T extends HasId>(items: T[], id: string): T | undefined {
  return items.find(item => item.id === id);
}
```

### Generic Components (React)
```typescript
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

function List<T>({ items, renderItem }: ListProps<T>) {
  return <ul>{items.map(renderItem)}</ul>;
}

// Usage
<List items={users} renderItem={(user) => <li>{user.name}</li>} />
```

## Null Safety

### Use Strict Null Checks
```typescript
// tsconfig.json: "strictNullChecks": true

function getUser(id: string): User | null {
  // ...
}

const user = getUser('123');
// user.name; // Error: Object is possibly null

if (user) {
  user.name; // OK - type narrowed
}

// Optional chaining
const name = user?.name;

// Nullish coalescing
const displayName = user?.name ?? 'Anonymous';
```

### Non-Null Assertion (Use Sparingly)
```typescript
// Only use when you're 100% certain
const element = document.getElementById('app')!;

// Better: Handle the null case
const element = document.getElementById('app');
if (!element) throw new Error('App element not found');
```

## Discriminated Unions

```typescript
interface LoadingState {
  status: 'loading';
}

interface SuccessState<T> {
  status: 'success';
  data: T;
}

interface ErrorState {
  status: 'error';
  error: string;
}

type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;

function handleState<T>(state: AsyncState<T>) {
  switch (state.status) {
    case 'loading':
      return <Spinner />;
    case 'success':
      return <Data data={state.data} />; // data is available
    case 'error':
      return <Error message={state.error} />; // error is available
  }
}
```

## Type Guards

### Custom Type Guards
```typescript
interface Dog {
  type: 'dog';
  bark(): void;
}

interface Cat {
  type: 'cat';
  meow(): void;
}

type Pet = Dog | Cat;

function isDog(pet: Pet): pet is Dog {
  return pet.type === 'dog';
}

function handlePet(pet: Pet) {
  if (isDog(pet)) {
    pet.bark(); // TypeScript knows pet is Dog
  } else {
    pet.meow(); // TypeScript knows pet is Cat
  }
}
```

### Assertion Functions
```typescript
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error('Value must be a string');
  }
}

function processInput(input: unknown) {
  assertIsString(input);
  // TypeScript knows input is string here
  console.log(input.toUpperCase());
}
```

## Async Patterns

```typescript
// Typed async functions
async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) throw new Error('Failed to fetch user');
  return response.json();
}

// Typed error handling
interface ApiError {
  code: string;
  message: string;
}

async function fetchData<T>(url: string): Promise<T> {
  const response = await fetch(url);

  if (!response.ok) {
    const error: ApiError = await response.json();
    throw new Error(error.message);
  }

  return response.json();
}
```

## Configuration

### Recommended tsconfig.json
```json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}
```

## Anti-Patterns to Avoid

```typescript
// DON'T: Use `any`
function process(data: any) {} // Defeats the purpose of TypeScript

// DO: Use `unknown` and narrow
function process(data: unknown) {
  if (typeof data === 'string') {
    // Now safely use as string
  }
}

// DON'T: Overuse type assertions
const user = {} as User; // Lies to TypeScript

// DO: Properly construct objects
const user: User = {
  id: '1',
  name: 'John',
  email: '[email protected]'
};

// DON'T: Use enums (they have quirks)
enum Status { Active, Inactive }

// DO: Use const objects or union types
const Status = { Active: 'active', Inactive: 'inactive' } as const;
type Status = typeof Status[keyof typeof Status];
```
Key Capabilities

What this rule helps you achieve

Type inference patternsGeneric typesUtility typesError handling
Tags
typescripttype-safetyfrontendbackend