typescript
/

TypeScript Best Practices – Write Clean & Maintainable Code

Last Sync: Today

On this page

15
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

typescript

TypeScript Best Practices – Write Clean & Maintainable Code

Why Best Practices Matter

TypeScript best practices help you write code that is type-safe, maintainable, and scalable. Following these guidelines reduces bugs, improves collaboration, and makes refactoring easier.

Use Strict Mode

JSONRead-only
1
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}

Prefer Interfaces Over Types

TypeScriptRead-only
1
// ✅ Good - Interface for objects
interface User {
  id: number;
  name: string;
  email: string;
}

// ✅ When to use type aliases
type ID = string | number;
type Nullable<T> = T | null;

// ❌ Avoid - Type for objects (unless you need union/intersection)
type UserType = {
  id: number;
  name: string;
};

Use const Assertions for Immutability

TypeScriptRead-only
1
// ✅ Good - Immutable configuration
const COLORS = ['red', 'green', 'blue'] as const;
const API_ENDPOINTS = {
  users: '/api/users',
  posts: '/api/posts'
} as const;

// Type: readonly ['red', 'green', 'blue']
// Type: { readonly users: '/api/users'; readonly posts: '/api/posts' }

Avoid any at All Costs

TypeScriptRead-only
1
// ❌ Bad - Using any
data: any;

// ✅ Good - Use unknown for uncertain types
data: unknown;

// ✅ Better - Use proper types
interface ApiResponse {
  data: User[];
  status: number;
}

// ✅ Use type guards
function isUser(data: unknown): data is User {
  return typeof data === 'object' && data !== null && 'id' in data;
}

Use Discriminated Unions

TypeScriptRead-only
1
// ✅ Good - Discriminated union pattern
interface SuccessResponse {
  status: 'success';
  data: User[];
}

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

type ApiResponse = SuccessResponse | ErrorResponse;

function handleResponse(response: ApiResponse) {
  if (response.status === 'success') {
    console.log(response.data); // TypeScript knows it's SuccessResponse
  } else {
    console.log(response.error); // TypeScript knows it's ErrorResponse
  }
}

Use Optional Chaining and Nullish Coalescing

TypeScriptRead-only
1
// ❌ Bad - Verbose null checks
const userName = user && user.profile && user.profile.name;
const displayName = userName !== null && userName !== undefined ? userName : 'Anonymous';

// ✅ Good - Modern TypeScript
const userName = user?.profile?.name;
const displayName = userName ?? 'Anonymous';

Prefer Readonly and as const

TypeScriptRead-only
1
// ✅ Good - Readonly arrays and properties
interface Config {
  readonly apiUrl: string;
  readonly timeout: number;
}

function processItems(items: readonly string[]) {
  // items.push('new'); // ❌ Error: Cannot modify
  items.forEach(item => console.log(item)); // ✅ OK
}

Use Generics for Reusable Code

TypeScriptRead-only
1
// ✅ Good - Generic function
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

// ✅ Good - Generic class
class Repository<T extends { id: number }> {
  private items: T[] = [];
  
  findById(id: number): T | undefined {
    return this.items.find(item => item.id === id);
  }
  
  save(item: T): void {
    this.items.push(item);
  }
}

Naming Conventions

ElementConventionExample
Variables/FunctionscamelCaseuserName, getUser()
Classes/InterfacesPascalCaseUserService, IUser (avoid I prefix)
ConstantsUPPER_SNAKE_CASEMAX_RETRY_COUNT
Private members_camelCase or camelCase_cache, private cache
Type parametersT, K, V or PascalCaseT, TData, TResponse

Use Utility Types

TypeScriptRead-only
1
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

// Partial - All properties optional
type UserUpdate = Partial<User>;

// Pick - Select specific properties
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;

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

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

// ReturnType - Extract function return type
function getUser(): User { /* ... */ }
type GetUserReturn = ReturnType<typeof getUser>; // User

Avoid Function Overloads When Possible

TypeScriptRead-only
1
// ❌ Bad - Complex overloads
function process(value: string): string;
function process(value: number): number;
function process(value: boolean): boolean;
function process(value: string | number | boolean): string | number | boolean {
  return value;
}

// ✅ Good - Generics or union types
function process<T>(value: T): T {
  return value;
}

// Or with conditional types
function process<T extends string | number | boolean>(value: T): T {
  return value;
}

Best Practices Summary Table

PracticeDoDon't
Type safetyUse strict mode, unknown, type guardsUse any, @ts-ignore
Object typesUse interfacesUse type aliases for objects
Null handlingOptional chaining (?.), nullish coalescing (??)Manual null checks, || for defaults
ImmutabilityUse readonly, as const, Readonly<T>Modify objects directly
ReusabilityUse generics, utility typesDuplicate code, any types

Common Anti-Patterns to Avoid

  • Using any - Destroys type safety completely
  • Overusing ! (non-null assertion) - Hides potential null errors
  • Using as type assertions unnecessarily - Bypasses type checking
  • Exporting types/interfaces unnecessarily - Increases bundle size
  • Using enums for runtime values - Prefer union types or as const
  • Ignoring TypeScript errors - Fix them, don't suppress them

Conclusion

Following TypeScript best practices leads to safer, more maintainable code. Enable strict mode, avoid any, use proper naming conventions, and leverage TypeScript's powerful type system. Remember: good TypeScript code is not just about making the compiler happy—it's about making your code understandable and reliable.

Try it yourself

// TypeScript Best Practices in Action

// ✅ Use strict typing
interface Product {
  readonly id: number;
  name: string;
  price: number;
  category: 'electronics' | 'clothing' | 'books';
}

// ✅ Use discriminated unions
type Result<T> = 
  | { success: true; data: T }
  | { success: false; error: string };

// ✅ Use generics
function fetchData<T>(url: string): Promise<Result<T>> {
  return fetch(url)
    .then(res => res.json())
    .then(data => ({ success: true, data }))
    .catch(err => ({ success: false, error: err.message }));
}

// ✅ Use optional chaining
const product: Product = {
  id: 1,
  name: 'Laptop',
  price: 999,
  category: 'electronics'
};

console.log(product?.name ?? 'Unknown product');
console.log(`Product ${product.name} is in category: ${product.category}`);

// Try modifying a readonly property (will error)
// product.id = 2; // ❌ Error: Cannot assign to 'id' because it is a read-only property

Test Your Knowledge

Q1
of 4

What should you use instead of 'any' for unknown types?

A
object
B
null
C
unknown
D
undefined
Q2
of 4

Which is preferred for defining object shapes?

A
type aliases
B
interfaces
C
classes
D
enums
Q3
of 4

What does the '??' operator do?

A
Logical OR
B
Nullish coalescing
C
Optional chaining
D
Type assertion
Q4
of 4

Which utility type makes all properties optional?

A
Pick
B
Omit
C
Partial
D
Required

Frequently Asked Questions

Interface or type alias?

Prefer interfaces for objects; use types for unions, intersections, and mapped types.

How to avoid any?

Use unknown, generics, or proper type definitions. Enable noImplicitAny.

What is strict mode?

Enables full type checking including null checks, no implicit any, and strict function types.

When to use as const?

When you need immutable, literal types for arrays or objects.

Previous

ts linting

Next

ts performance

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.