Senyo Labs | Yazılım & Teknoloji Çözümleri
Bloga Dön
Mühendislik10 okuma25 Eylül 2024

TypeScript Best Practices for 2024

Learn modern TypeScript patterns and best practices to write type-safe, maintainable code that scales with your application.

Ömer Faruk Genç

Ömer Faruk Genç

Full Stack Engineer

#TypeScript#Programming#Best Practices
TypeScript Best Practices for 2024

TypeScript Best Practices for 2024

TypeScript has become the standard for building large-scale JavaScript applications. Let's explore modern best practices that will help you write better, more maintainable TypeScript code.

Why TypeScript?

TypeScript adds static typing to JavaScript, providing:

  • Early Error Detection: Catch bugs at compile time, not runtime
  • Better IDE Support: Autocomplete, refactoring, and navigation
  • Improved Code Documentation: Types serve as inline documentation
  • Enhanced Maintainability: Easier to understand and modify code

Essential Best Practices

1. Use Strict Mode

Always enable strict mode in your tsconfig.json:

{ "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true } }

This catches more errors and enforces better practices.

2. Prefer Interfaces for Object Shapes

Use interfaces for object types, especially when extending:

// Good interface User { id: string; name: string; email: string; } interface Admin extends User { permissions: string[]; } // Also fine for unions and intersections type Status = 'active' | 'inactive' | 'pending';

3. Use Union Types for Discriminated Unions

Create type-safe state machines with discriminated unions:

type ApiResponse<T> = | { status: 'loading' } | { status: 'error'; error: string } | { status: 'success'; data: T }; function handleResponse<T>(response: ApiResponse<T>) { switch (response.status) { case 'loading': return 'Loading...'; case 'error': return `Error: ${response.error}`; case 'success': return response.data; // TypeScript knows data exists } }

4. Leverage Type Guards

Create custom type guards for runtime type checking:

interface Cat { type: 'cat'; meow: () => void; } interface Dog { type: 'dog'; bark: () => void; } type Animal = Cat | Dog; function isCat(animal: Animal): animal is Cat { return animal.type === 'cat'; } function makeSound(animal: Animal) { if (isCat(animal)) { animal.meow(); // TypeScript knows it's a Cat } else { animal.bark(); // TypeScript knows it's a Dog } }

5. Use const Assertions

Preserve literal types with const assertions:

// Without const assertion const colors = ['red', 'green', 'blue']; // string[] // With const assertion const colors = ['red', 'green', 'blue'] as const; // readonly ["red", "green", "blue"] type Color = typeof colors[number]; // "red" | "green" | "blue"

6. Utility Types Are Your Friends

TypeScript provides powerful utility types:

interface User { id: string; name: string; email: string; age: number; } // Pick specific properties type UserPreview = Pick<User, 'id' | 'name'>; // { id: string; name: string; } // Omit properties type UserWithoutEmail = Omit<User, 'email'>; // Make all properties optional type PartialUser = Partial<User>; // Make all properties required type RequiredUser = Required<PartialUser>; // Make all properties readonly type ReadonlyUser = Readonly<User>; // Create a record type type UserRoles = Record<string, User>;

7. Generic Constraints

Use generic constraints to make your functions more flexible yet type-safe:

// Bad: Too loose function getValue(obj: any, key: string) { return obj[key]; } // Good: Type-safe with generics function getValue<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } const user = { name: 'John', age: 30 }; const name = getValue(user, 'name'); // Type: string const age = getValue(user, 'age'); // Type: number // getValue(user, 'invalid'); // Error: Argument of type 'invalid' is not assignable

8. Avoid Type Assertions (When Possible)

Type assertions bypass TypeScript's type checking:

// Bad: Dangerous const data = apiResponse as User; // Better: Validate and type guard function isUser(data: unknown): data is User { return ( typeof data === 'object' && data !== null && 'id' in data && 'name' in data && 'email' in data ); } const data = apiResponse; if (isUser(data)) { // Now TypeScript knows data is User console.log(data.name); }

9. Use Template Literal Types

Create powerful string types:

type Endpoint = 'users' | 'posts' | 'comments'; type Method = 'GET' | 'POST' | 'PUT' | 'DELETE'; type ApiRoute = `/api/${Endpoint}`; // "/api/users" | "/api/posts" | "/api/comments" type ApiMethod = `${Method} ${ApiRoute}`; // "GET /api/users" | "POST /api/users" | ... function callApi(route: ApiRoute, method: Method) { // Type-safe API calls } callApi('/api/users', 'GET'); // Valid // callApi('/api/invalid', 'GET'); // Error

10. Organize Types with Namespaces

Keep related types together:

namespace Api { export interface Request { method: string; url: string; headers?: Record<string, string>; } export interface Response<T> { status: number; data: T; } export type ErrorResponse = Response<{ message: string }>; } function makeRequest(req: Api.Request): Promise<Api.Response<unknown>> { // Implementation }

Advanced Patterns

Branded Types

Create nominal types in TypeScript's structural type system:

type UserId = string & { readonly brand: unique symbol }; type ProductId = string & { readonly brand: unique symbol }; function getUserById(id: UserId) { // Implementation } const userId = 'user-123' as UserId; const productId = 'prod-456' as ProductId; getUserById(userId); // OK // getUserById(productId); // Error: Type 'ProductId' is not assignable to type 'UserId'

Builder Pattern

Type-safe builders with method chaining:

class QueryBuilder<T> { private filters: ((item: T) => boolean)[] = []; where(predicate: (item: T) => boolean): this { this.filters.push(predicate); return this; } execute(data: T[]): T[] { return data.filter(item => this.filters.every(filter => filter(item)) ); } } const users = new QueryBuilder<User>() .where(u => u.age > 18) .where(u => u.active) .execute(allUsers);

Conclusion

TypeScript is a powerful tool that, when used correctly, can dramatically improve your code quality and developer experience. These best practices will help you:

  • Write more maintainable code
  • Catch bugs earlier
  • Improve team collaboration
  • Scale your applications with confidence

Remember: good TypeScript code is about finding the right balance between type safety and pragmatism. Don't over-engineer your types, but don't shy away from leveraging TypeScript's powerful features when they add real value.

Start applying these practices today, and watch your TypeScript code become more robust, maintainable, and enjoyable to work with!