TypeScript Best Practices for 2024
Learn the most effective TypeScript patterns and practices for building robust applications.
TypeScript Best Practices for 2024
TypeScript has become the standard for building large-scale JavaScript applications. In this guide, we'll explore the best practices that will help you write more maintainable, type-safe, and efficient TypeScript code.
1. Use Strict Mode
Always enable strict mode in your tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true
}
}
2. Prefer Interfaces Over Types
Use interfaces for object shapes and types for unions, primitives, and complex types:
// Good - Interface for object shape
interface User {
id: string
name: string
email: string
}
// Good - Type for union
type Status = 'loading' | 'success' | 'error'
// Good - Type for complex transformations
type UserWithoutId = Omit<User, 'id'>
3. Use Generic Constraints
When using generics, always add constraints to make them more specific:
// Bad
function getProperty<T>(obj: T, key: string) {
return obj[key] // Error: Element implicitly has an 'any' type
}
// Good
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key] // Type-safe!
}
// Usage
const user = { name: 'John', age: 30 }
const name = getProperty(user, 'name') // Type: string
4. Leverage Utility Types
TypeScript provides powerful utility types that can save you time:
interface User {
id: string
name: string
email: string
password: string
createdAt: Date
updatedAt: Date
}
// Create a type for API responses (without password)
type UserResponse = Omit<User, 'password'>
// Create a type for creating new users (without id and timestamps)
type CreateUserRequest = Pick<User, 'name' | 'email' | 'password'>
// Make all properties optional
type PartialUser = Partial<User>
// Make all properties required
type RequiredUser = Required<User>
// Extract specific properties
type UserCredentials = Pick<User, 'email' | 'password'>
5. Use Discriminated Unions
For better type safety with related types:
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string }
// Usage
function handleRequest<T>(state: RequestState<T>) {
switch (state.status) {
case 'idle':
return 'Ready to make request'
case 'loading':
return 'Loading...'
case 'success':
return `Data: ${state.data}` // Type-safe access to data
case 'error':
return `Error: ${state.error}` // Type-safe access to error
}
}
6. Prefer const Assertions
Use const assertions for immutable data:
// Bad
const colors = ['red', 'green', 'blue'] // Type: string[]
// Good
const colors = ['red', 'green', 'blue'] as const // Type: readonly ["red", "green", "blue"]
// Usage
type Color = typeof colors[number] // Type: "red" | "green" | "blue"
7. Use Function Overloads Sparingly
Function overloads can be useful but often make code harder to understand:
// Prefer union types over overloads
function formatValue(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase()
}
return value.toString()
}
// Only use overloads when necessary for complex APIs
interface ApiClient {
get<T>(url: string): Promise<T>
post<T>(url: string, data: any): Promise<T>
}
8. Use Branded Types for Type Safety
Create branded types for better type safety:
// Branded type for user IDs
type UserId = string & { readonly brand: unique symbol }
// Branded type for email addresses
type Email = string & { readonly brand: unique symbol }
// Helper functions
function createUserId(id: string): UserId {
return id as UserId
}
function createEmail(email: string): Email {
if (!email.includes('@')) {
throw new Error('Invalid email')
}
return email as Email
}
// Usage
function getUser(id: UserId) {
// Only accepts UserId, not any string
}
const userId = createUserId('123')
getUser(userId) // OK
getUser('123') // Error: Argument of type 'string' is not assignable to parameter of type 'UserId'
9. Use Template Literal Types
Leverage template literal types for string manipulation:
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type ApiEndpoint = `/api/${string}`
type ApiUrl = `${HttpMethod} ${ApiEndpoint}`
// Usage
function makeRequest(url: ApiUrl) {
// Type-safe API URLs
}
makeRequest('GET /api/users') // OK
makeRequest('POST /api/users') // OK
makeRequest('GET /users') // Error: Type '"GET /users"' is not assignable to type 'ApiUrl'
10. Use Conditional Types
Conditional types can make your code more flexible:
type NonNullable<T> = T extends null | undefined ? never : T
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}
type AsyncReturnType<T> = T extends (...args: any[]) => Promise<infer R> ? R : never
// Usage
type User = {
id: string
profile: {
name: string
avatar: string
}
}
type PartialUser = DeepPartial<User>
// Result:
// {
// id?: string
// profile?: {
// name?: string
// avatar?: string
// }
// }
11. Use satisfies Operator
The satisfies operator ensures type safety while preserving literal types:
// Without satisfies
const config = {
api: {
baseUrl: 'https://api.example.com',
timeout: 5000
}
} // Type is inferred as { api: { baseUrl: string; timeout: number } }
// With satisfies
const config = {
api: {
baseUrl: 'https://api.example.com',
timeout: 5000
}
} satisfies Config // Type is preserved as literal types
// Usage
config.api.baseUrl // Type: "https://api.example.com" (literal type)
config.api.timeout // Type: 5000 (literal type)
12. Use never Type Appropriately
Use never for exhaustive checks and impossible states:
function exhaustiveCheck(value: never): never {
throw new Error(`Unhandled value: ${value}`)
}
function handleStatus(status: 'loading' | 'success' | 'error') {
switch (status) {
case 'loading':
return 'Loading...'
case 'success':
return 'Success!'
case 'error':
return 'Error!'
default:
return exhaustiveCheck(status) // Ensures all cases are handled
}
}
Conclusion
These TypeScript best practices will help you write more maintainable, type-safe, and efficient code. Remember to:
- Always use strict mode
- Prefer interfaces for object shapes
- Use utility types to avoid repetition
- Leverage discriminated unions for better type safety
- Use branded types for domain-specific types
- Keep your types simple and readable
TypeScript is a powerful tool, and using it effectively can significantly improve your development experience and code quality.
Resources
Happy coding! 🚀
Project Links
Related Posts
Mastering React Hooks: A Complete Guide
Learn how to use React hooks effectively to build better components and manage state.
CSS Grid vs Flexbox: When to Use Each
A comprehensive comparison of CSS Grid and Flexbox, with practical examples and use cases.
Getting Started with Next.js 15
Learn how to build modern web applications with Next.js 15 and the App Router.