TypeScript Best Practices

Writing TypeScript code is one thing, but writing maintainable, efficient TypeScript that stands the test of time requires following proven practices. These guidelines help teams collaborate effectively while avoiding common pitfalls that can make TypeScript code harder to work with. Whether you're working on a small project or a large enterprise application, these best practices will help you get the most value from TypeScript's type system.

Keeping Code Clean with TypeScript

Use Strict Mode Always

Enable all strict checks in your tsconfig.json:

json
1
2
3
4
5
6
7
8
9
10
          {
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true
  }
}
        

This catches potential runtime errors during development and forces you to write more explicit code.

Prefer Interfaces Over Type Aliases When Possible

typescript
1
2
3
4
5
6
7
8
          // Good
interface User {
  id: string;
  name: string;
}

// Only use type aliases for unions, tuples, or complex types
type UserRole = 'admin' | 'user' | 'guest';
        

Interfaces are more extensible (can be merged with declaration merging) and show clearer error messages.

Use Type Inference Where Possible

Don't add redundant type annotations:

typescript
1
2
3
4
5
          // Bad
const count: number = 0;

// Good
const count = 0; // TypeScript knows this is a number
        

Keep Function Signatures Clean

typescript
1
2
3
4
5
6
7
8
9
          // Bad
function processData(data: any): any {
  // ...
}

// Good
function processData<T>(data: T): ProcessedResult<T> {
  // ...
}
        

Use Utility Types Wisely

typescript
1
2
3
4
5
6
7
8
9
10
11
12
          interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}

// When updating, make all fields optional
type UserUpdate = Partial<User>;

// When creating, omit auto-generated fields
type UserCreate = Omit<User, 'id' | 'createdAt'>;
        

Avoiding Common TypeScript Mistakes

Don't Overuse `any`

typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
          // Bad - loses all type safety
function logValue(value: any) {
  console.log(value);
}

// Better
function logValue<T>(value: T) {
  console.log(value);
}

// Best (when you need runtime type checking)
function logValue(value: unknown) {
  if (typeof value === 'string') {
    console.log(value.toUpperCase());
  }
}
        

Avoid Non-Null Assertions (`!`) Unless Absolutely Necessary

typescript
1
2
3
4
5
6
7
          // Bad - might lead to runtime errors
const element = document.getElementById('my-element')!;

// Better
const element = document.getElementById('my-element');
if (!element) throw new Error('Element not found');
// Now TypeScript knows element exists
        

Be Careful with Enums

typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
          // Avoid numeric enums
enum BadStatus {
  Active, // 0
  Inactive // 1
}

// Prefer string enums or union types
type BetterStatus = 'active' | 'inactive';

// Or if you need enums
enum GoodStatus {
  Active = 'active',
  Inactive = 'inactive'
}
        

Don't Mix Type Systems

typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
          // Bad - mixing class validator decorators with TypeScript types
class User {
  @IsString()
  name: string;

  @IsEmail()
  email: string;
}

// Better - keep runtime validation separate
interface User {
  name: string;
  email: string;
}

class UserValidator {
  static validate(user: User) {
    // Validation logic
  }
}
        

Organizing Your TypeScript Project Structure

Recommended Folder Structure

typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
          src/
├── core/           # Framework-agnostic core logic
├── features/       # Feature modules
│   ├── users/
│   │   ├── types.ts
│   │   ├── api.ts
│   │   ├── components/
│   │   └── utils.ts
├── shared/         # Shared utilities and types
│   ├── lib/
│   ├── types/
│   └── utils/
├── app.ts          # Main application entry
└── config.ts       # Configuration
        

Type Organization Strategies

1. Co-locate types with features:

typescript
1
2
3
4
5
6
7
8
9
10
          // features/users/types.ts
interface User {
  id: string;
  name: string;
}

type UserRole = 'admin' | 'user';

// Export all user-related types
export type { User, UserRole };
        

2. Use a central types folder for shared types:

typescript
1
2
3
4
5
6
7
8
          // shared/types/api.ts
interface ApiResponse<T> {
  data: T;
  status: number;
  timestamp: Date;
}

export type { ApiResponse };
        

3. Consider namespaces for large projects:

typescript
1
2
3
4
5
6
7
8
9
10
          // shared/types/models.ts
namespace Models {
  export interface Product {
    id: string;
    name: string;
  }
}

// Later usage
const product: Models.Product = { ... };
        

Configuration Management

1. Environment variables:

typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
          // config/env.ts
interface Env {
  PORT: number;
  DB_URL: string;
  NODE_ENV: 'development' | 'production';
}

const env: Env = {
  PORT: parseInt(process.env.PORT || '3000'),
  DB_URL: process.env.DB_URL || 'localhost',
  NODE_ENV: process.env.NODE_ENV as 'development' | 'production' || 'development'
};

export default env;
        

2. Feature flags:

typescript
1
2
3
4
5
6
7
8
9
10
11
12
          // shared/types/features.ts
type FeatureFlags = {
  newDashboard: boolean;
  experimentalAPI: boolean;
  darkMode: boolean;
};

const features: FeatureFlags = {
  newDashboard: true,
  experimentalAPI: false,
  darkMode: true
};
        

Documentation Practices

1. Use TSDoc for important types:

typescript
1
2
3
4
5
6
7
8
9
10
11
          /**
 * Represents a user in our system
 * @property id - Unique identifier
 * @property name - User's full name
 * @property email - User's email address
 */
interface User {
  id: string;
  name: string;
  email: string;
}
        

2. Create a README.md in your types folder:

# Type Definitions This directory contains all type definitions for the project.

Structure

typescript
1
          
        
  • core/ - Types for domain models
  • api/ - Types for API responses
  • ui/ - Types for component props

Testing Considerations

1. Type tests:

typescript
1
2
3
4
5
6
7
8
9
          // tests/types/user.test.ts
import { User } from '../../src/features/users/types';

// This test will fail if the User type changes unexpectedly
const testUser: User = {
  id: '1',
  name: 'Test',
  email: 'test@example.com'
} satisfies User;
        

2. Mock data typing:

typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
          // tests/mocks/users.ts
import { User } from '../../src/features/users/types';

export const mockUser: User = {
  id: 'user-1',
  name: 'Mock User',
  email: 'mock@example.com'
};

// Factory function
export function createMockUser(overrides?: Partial<User>): User {
  return {
    ...mockUser,
    ...overrides
  };
}
        

By following these best practices, your TypeScript code will be more maintainable, less prone to errors, and easier for teams to collaborate on. The key is finding the right balance between type safety and flexibility while keeping your codebase organized as it grows.