TypeScript Best Practices
TypeScript has become an essential tool for modern web development, providing static type checking that catches errors at compile time and enhances developer productivity.
Why TypeScript?
TypeScript adds optional static typing to JavaScript, which brings several benefits:
- Early Error Detection: Catch bugs at compile time instead of runtime
- Better IDE Support: Enhanced autocomplete, refactoring, and navigation
- Self-Documenting Code: Types serve as inline documentation
- Safer Refactoring: Confidence when changing code across large codebases
Essential Type Patterns
Interface vs Type Aliases
Use interfaces for object shapes that might be extended:
interface User {
id: string
name: string
email: string
}
interface AdminUser extends User {
permissions: string[]
}
Use type aliases for unions, primitives, and computed types:
type Status = 'loading' | 'success' | 'error'
type EventHandler<T> = (event: T) => void
type UserKeys = keyof User // 'id' | 'name' | 'email'
Generic Types
Create reusable, type-safe functions and components:
function identity<T>(arg: T): T {
return arg
}
interface ApiResponse<T> {
data: T
status: number
message: string
}
// Usage
const userResponse: ApiResponse<User> = await fetchUser(id)
Utility Types
Leverage TypeScript's built-in utility types:
// Pick specific properties
type UserPreview = Pick<User, 'id' | 'name'>
// Make all properties optional
type PartialUser = Partial<User>
// Make all properties required
type RequiredUser = Required<User>
// Exclude certain properties
type UserWithoutId = Omit<User, 'id'>
Advanced Patterns
Discriminated Unions
Create type-safe state management:
type LoadingState = {
status: 'loading'
}
type SuccessState = {
status: 'success'
data: User[]
}
type ErrorState = {
status: 'error'
error: string
}
type AppState = LoadingState | SuccessState | ErrorState
// Type-safe state handling
function handleState(state: AppState) {
switch (state.status) {
case 'loading':
return <Spinner />
case 'success':
return <UserList users={state.data} /> // TypeScript knows data exists
case 'error':
return <ErrorMessage error={state.error} /> // TypeScript knows error exists
}
}
Conditional Types
Create types that depend on other types:
type NonNullable<T> = T extends null | undefined ? never : T
type ApiEndpoint<T> = T extends 'users'
? '/api/users'
: T extends 'posts'
? '/api/posts'
: never
// Usage
type UsersEndpoint = ApiEndpoint<'users'> // '/api/users'
Template Literal Types
Build string types programmatically:
type EventName<T extends string> = `on${Capitalize<T>}`
type ButtonEvents = EventName<'click' | 'hover'> // 'onClick' | 'onHover'
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type Endpoint = `/api/${string}`
type ApiCall = `${HttpMethod} ${Endpoint}`
Best Practices
1. Use Strict TypeScript Configuration
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
2. Prefer Type Assertions Over Type Casting
// Good: Type assertion
const userInput = document.getElementById('user-input') as HTMLInputElement
// Avoid: Type casting
const userInput = <HTMLInputElement>document.getElementById('user-input')
3. Use Type Guards for Runtime Safety
function isUser(obj: unknown): obj is User {
return typeof obj === 'object' && obj !== null && 'id' in obj
}
function processData(data: unknown) {
if (isUser(data)) {
// TypeScript knows data is User here
console.log(data.name)
}
}
4. Leverage const Assertions
// Creates a readonly tuple type
const colors = ['red', 'green', 'blue'] as const
type Color = typeof colors[number] // 'red' | 'green' | 'blue'
// Creates exact object type
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000
} as const
Common Pitfalls to Avoid
- Using
any
: Defeats the purpose of TypeScript - Excessive type assertions: Often indicates design issues
- Ignoring compiler errors: Address them instead of suppressing
- Over-engineering types: Keep it simple and readable
Conclusion
TypeScript's type system is powerful and flexible. By following these patterns and best practices, you can write more maintainable, bug-free code while enjoying excellent developer experience.
The key is to gradually adopt TypeScript's features, starting with basic typing and progressively using more advanced patterns as your understanding grows.