Back to Blog
TypeScript Best Practices for 2024
Master TypeScript with modern best practices, advanced types, and patterns that will make your code more maintainable, type-safe, and developer-friendly.
Akash Aman
July 25, 2024
15 min read
TypeScript
Web Development
JavaScript
Frontend
TypeScript Best Practices for 2024
TypeScript continues to evolve rapidly, and staying current with best practices is crucial for writing maintainable, type-safe code.
Modern TypeScript Features
1. Satisfies Operator
type Colors = 'red' | 'green' | 'blue'
const palette = {
red: '#ff0000',
green: '#00ff00',
blue: '#0000ff',
// This would cause an error without 'satisfies'
} satisfies Record<Colors, string>
// Now you get both type checking and intellisense
const redColor = palette.red // string type, not Colors
2. Template Literal Types
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type Endpoint = '/api/${string}'
type APIRoute = `${HTTPMethod} ${Endpoint}`
// Usage
function makeRequest<T extends APIRoute>(route: T) {
// Type-safe API calls
}
makeRequest('GET /api/users') // ✅ Valid
makeRequest('INVALID /api/users') // ❌ Type error
3. Conditional Types
type NonNullable<T> = T extends null | undefined ? never : T
type ApiResponse<T> = T extends string
? { message: T }
: T extends object
? { data: T }
: never
type UserResponse = ApiResponse<User> // { data: User }
type ErrorResponse = ApiResponse<string> // { message: string }
Advanced Patterns
1. Branded Types
type UserId = string & { readonly __brand: unique symbol }
type Email = string & { readonly __brand: unique symbol }
function createUserId(id: string): UserId {
return id as UserId
}
function sendEmail(userId: UserId, email: Email) {
// Type-safe function that prevents mixing up parameters
}
2. Discriminated Unions
type LoadingState = {
status: 'loading'
}
type SuccessState = {
status: 'success'
data: any
}
type ErrorState = {
status: 'error'
error: string
}
type AsyncState = LoadingState | SuccessState | ErrorState
function handleState(state: AsyncState) {
switch (state.status) {
case 'loading':
// TypeScript knows this is LoadingState
return <Spinner />
case 'success':
// TypeScript knows state.data exists
return <Data data={state.data} />
case 'error':
// TypeScript knows state.error exists
return <Error message={state.error} />
}
}
3. Utility Types Mastery
// Pick specific properties
type UserPreview = Pick<User, 'id' | 'name' | 'email'>
// Omit properties
type CreateUserRequest = Omit<User, 'id' | 'createdAt'>
// Make all properties optional
type PartialUser = Partial<User>
// Make all properties required
type RequiredUser = Required<PartialUser>
// Extract keys that extend a condition
type StringKeys<T> = {
[K in keyof T]: T[K] extends string ? K : never
}[keyof T]
Configuration Best Practices
1. Strict TypeScript Config
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true
}
}
2. Path Mapping
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@/components/*": ["src/components/*"],
"@/utils/*": ["src/utils/*"]
}
}
}
Testing with TypeScript
1. Type-safe Test Utilities
import { expectTypeOf } from 'vitest'
// Test types at compile time
test('User type should have correct shape', () => {
expectTypeOf<User>().toMatchTypeOf<{
id: string
name: string
email: string
}>()
})
2. Mock Types
type MockedFunction<T extends (...args: any[]) => any> = jest.MockedFunction<T>
const mockUserService: MockedFunction<typeof userService.getUser> =
jest.fn()
Performance Tips
- Use Type Imports:
import type { User } from './types'
- Avoid Enum Overhead: Use const assertions instead
- Leverage Tree Shaking: Import only what you need
- Use Interface over Type: For object shapes, interfaces are more performant
Common Pitfalls to Avoid
- Any Escape Hatch: Resist using
any
, useunknown
instead - Non-null Assertion Overuse: Avoid
!
operator without proper checks - Ignoring Compiler Errors: Address TypeScript errors properly
- Over-engineering Types: Keep types simple and readable
TypeScript in 2024 is more powerful than ever. These practices will help you write better, more maintainable code while leveraging the full power of the type system.