TypeScript Best Practices for Production Applications
Learn essential TypeScript best practices and patterns to write type-safe, maintainable code for production-ready applications.
TypeScript Best Practices for Production Applications
TypeScript has become the de facto standard for building large-scale JavaScript applications. In this guide, I'll share best practices and patterns that I've learned from building production applications.
1. Leverage Type Inference
TypeScript's type inference is powerful. Don't over-annotate when TypeScript can infer types automatically:
// ❌ Redundant type annotation
const message: string = 'Hello, World!'
const numbers: number[] = [1, 2, 3]
// ✅ Let TypeScript infer
const message = 'Hello, World!'
const numbers = [1, 2, 3]
// ✅ Annotate when needed
function greet(name: string): string {
return `Hello, ${name}!`
}
2. 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
}
}
3. Prefer Interfaces Over Type Aliases for Objects
Interfaces are more performant and provide better error messages:
// ✅ Preferred
interface User {
id: string
name: string
email: string
}
// ⚠️ Use for unions, intersections, or primitives
type Status = 'active' | 'inactive' | 'pending'
type ID = string | number
4. Use Discriminated Unions
Discriminated unions make type narrowing powerful and safe:
type Success<T> = {
status: 'success'
data: T
}
type Error = {
status: 'error'
error: string
}
type Result<T> = Success<T> | Error
function handleResult<T>(result: Result<T>) {
if (result.status === 'success') {
// TypeScript knows result.data exists
console.log(result.data)
} else {
// TypeScript knows result.error exists
console.error(result.error)
}
}
5. Avoid the any Type
The any type defeats the purpose of TypeScript. Use unknown instead:
// ❌ Avoid
function processValue(value: any) {
return value.toString() // No type safety
}
// ✅ Better
function processValue(value: unknown) {
if (typeof value === 'string') {
return value.toUpperCase()
}
if (typeof value === 'number') {
return value.toFixed(2)
}
throw new Error('Unsupported type')
}
6. Use Utility Types
TypeScript provides powerful utility types. Master them:
interface User {
id: string
name: string
email: string
password: string
}
// Pick specific properties
type PublicUser = Pick<User, 'id' | 'name'>
// Omit properties
type UserWithoutPassword = Omit<User, 'password'>
// Make all properties optional
type PartialUser = Partial<User>
// Make all properties required
type RequiredUser = Required<User>
// Make all properties readonly
type ReadonlyUser = Readonly<User>
7. Use const Assertions
Const assertions provide more precise types:
// Without const assertion
const colors = ['red', 'green', 'blue']
// Type: string[]
// With const assertion
const colors = ['red', 'green', 'blue'] as const
// Type: readonly ["red", "green", "blue"]
// Great for configuration objects
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
} as const
// All properties are readonly
8. Generic Constraints
Use generic constraints to make your functions more flexible and type-safe:
interface HasId {
id: string
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id)
}
// Works with any object that has an id
const users = [
{ id: '1', name: 'Alice' },
{ id: '2', name: 'Bob' },
]
const user = findById(users, '1') // Type: { id: string; name: string } | undefined
9. Type Guards
Create custom type guards for better type narrowing:
interface Dog {
type: 'dog'
bark(): void
}
interface Cat {
type: 'cat'
meow(): void
}
type Pet = Dog | Cat
// Type guard
function isDog(pet: Pet): pet is Dog {
return pet.type === 'dog'
}
function handlePet(pet: Pet) {
if (isDog(pet)) {
pet.bark() // TypeScript knows pet is Dog
} else {
pet.meow() // TypeScript knows pet is Cat
}
}
10. Avoid Enums (Use Union Types Instead)
Union types are more flexible and tree-shakeable:
// ❌ Enum (adds runtime code)
enum Status {
Active = 'ACTIVE',
Inactive = 'INACTIVE',
Pending = 'PENDING',
}
// ✅ Union type (no runtime cost)
const Status = {
Active: 'ACTIVE',
Inactive: 'INACTIVE',
Pending: 'PENDING',
} as const
type Status = (typeof Status)[keyof typeof Status]
11. Use Template Literal Types
Template literal types are powerful for creating precise string types:
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type Endpoint = '/users' | '/posts' | '/comments'
// Create all possible combinations
type Route = `${HTTPMethod} ${Endpoint}`
// 'GET /users' | 'POST /users' | 'GET /posts' | ...
type EventName = 'click' | 'focus' | 'blur'
type EventHandler = `on${Capitalize<EventName>}`
// 'onClick' | 'onFocus' | 'onBlur'
12. Proper Error Handling
Type your errors properly:
class AppError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number = 500
) {
super(message)
this.name = 'AppError'
}
}
function handleError(error: unknown): AppError {
if (error instanceof AppError) {
return error
}
if (error instanceof Error) {
return new AppError(error.message, 'UNKNOWN_ERROR')
}
return new AppError('An unknown error occurred', 'UNKNOWN_ERROR')
}
Conclusion
These TypeScript best practices will help you write more maintainable, type-safe code. Remember:
- Enable strict mode
- Leverage type inference
- Use discriminated unions
- Avoid
any, preferunknown - Master utility types
- Create custom type guards
TypeScript is a powerful tool that becomes even more valuable when used correctly. Start implementing these practices in your projects today!
Additional Resources
Happy coding! 🚀
Related Posts
Building pyfs-watcher: A Rust-Powered Filesystem Toolkit for Python
How I built a high-performance filesystem toolkit for Python using Rust and PyO3 — parallel directory walking, BLAKE3 hashing, file deduplication, and real-time watching, all from pip install.
How I Built This Developer Portfolio
A technical walkthrough of building my portfolio site with Next.js 15, TypeScript, Tailwind CSS, and a dual-source blog system powered by MDX files and SQLite.
React Performance Optimization: A Complete Guide
Master React performance optimization techniques including memo, useMemo, useCallback, code splitting, and more to build blazing-fast applications.
Enjoyed this post?
Subscribe to get notified when I publish new content about web development and technology.