TypeScript Best Practices
Professional patterns for TypeScript development
⭐ What are Best Practices?
TypeScript best practices are proven patterns that help you write safer, more maintainable code. They prevent common bugs, improve code quality, and make your applications easier to scale and debug.
// Good: Strict type checking
interface User {
id: number
name: string
email: string
}
const user: User = { id: 1, name: 'John', email: '[email protected]' }
Core Principles
Type Safety
Always use strict types
// Enable strict mode
// tsconfig.json
"strict": true
Avoid Any
Don't use 'any' type
// Bad: any
let data: any
// Good: specific type
let data: User
Null Checks
Handle null and undefined
if (user !== null) {
console.log(user.name)
}
Immutability
Use readonly when possible
interface Config {
readonly apiKey: string
}
🔹 Enable Strict Mode
Always use strict TypeScript compiler options:
// tsconfig.json
{
"compilerOptions": {
"strict": true, // Enable all strict checks
"noImplicitAny": true, // No implicit any types
"strictNullChecks": true, // Strict null checking
"strictFunctionTypes": true, // Strict function types
"strictBindCallApply": true, // Strict bind/call/apply
"noUnusedLocals": true, // Error on unused variables
"noUnusedParameters": true, // Error on unused parameters
"noImplicitReturns": true, // All code paths return
"noFallthroughCasesInSwitch": true // No fallthrough in switch
}
}
Benefits:
- Catches more errors at compile time
- Prevents null/undefined bugs
- Enforces better code quality
🔹 Avoid Using 'any'
Use specific types instead of 'any':
// ✗ Bad: Using any defeats TypeScript's purpose
function processData(data: any): any {
return data.value
}
// ✓ Good: Use specific types
interface DataItem {
value: string
id: number
}
function processData(data: DataItem): string {
return data.value
}
// ✓ Good: Use unknown for truly unknown types
function parseJSON(json: string): unknown {
return JSON.parse(json)
}
// ✓ Good: Use generics for flexible types
function identity<T>(value: T): T {
return value
}
// ✓ Good: Use union types for multiple possibilities
function formatValue(value: string | number): string {
return String(value)
}
🔹 Handle Null and Undefined
Always check for null and undefined values:
// ✗ Bad: No null check
function getUserName(user: User | null): string {
return user.name // Error if user is null
}
// ✓ Good: Check for null
function getUserName(user: User | null): string {
if (user === null) {
return 'Guest'
}
return user.name
}
// ✓ Good: Use optional chaining
function getUserEmail(user: User | null): string | undefined {
return user?.email
}
// ✓ Good: Use nullish coalescing
function getDisplayName(name: string | null | undefined): string {
return name ?? 'Anonymous'
}
// ✓ Good: Type guard function
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'name' in value &&
'email' in value
)
}
if (isUser(data)) {
console.log(data.name) // TypeScript knows data is User
}
🔹 Use Readonly for Immutability
Prevent accidental mutations:
// ✓ Good: Readonly properties
interface Config {
readonly apiUrl: string
readonly timeout: number
}
const config: Config = {
apiUrl: 'https://api.example.com',
timeout: 5000
}
// config.apiUrl = 'new-url' // ✗ Error: readonly
// ✓ Good: Readonly arrays
const numbers: readonly number[] = [1, 2, 3, 4, 5]
// numbers.push(6) // ✗ Error: readonly
// ✓ Good: ReadonlyArray type
const names: ReadonlyArray<string> = ['Alice', 'Bob']
// ✓ Good: Readonly utility type
interface User {
name: string
age: number
}
const user: Readonly<User> = {
name: 'John',
age: 30
}
// user.age = 31 // ✗ Error: readonly
🔹 Use Type Guards
Safely narrow types at runtime:
// ✓ Good: typeof type guard
function processValue(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase()
}
return value.toFixed(2)
}
// ✓ Good: instanceof type guard
class Dog {
bark() { console.log('Woof!') }
}
class Cat {
meow() { console.log('Meow!') }
}
function makeSound(animal: Dog | Cat): void {
if (animal instanceof Dog) {
animal.bark()
} else {
animal.meow()
}
}
// ✓ Good: Custom type guard
interface Fish {
swim: () => void
}
interface Bird {
fly: () => void
}
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined
}
function move(pet: Fish | Bird): void {
if (isFish(pet)) {
pet.swim()
} else {
pet.fly()
}
}
🔹 Prefer Interfaces Over Types
Use interfaces for object shapes:
// ✓ Good: Interface for objects
interface User {
id: number
name: string
email: string
}
// ✓ Good: Interfaces can be extended
interface Admin extends User {
role: string
permissions: string[]
}
// ✓ Good: Interfaces can be merged
interface Window {
customProperty: string
}
// ✓ Use type for unions and primitives
type Status = 'active' | 'inactive' | 'pending'
type ID = string | number
// ✓ Use type for complex types
type Callback = (data: string) => void
type Nullable<T> = T | null
🔹 Use Enums Wisely
Choose the right enum type:
// ✓ Good: String enums for better debugging
enum Status {
Active = 'ACTIVE',
Inactive = 'INACTIVE',
Pending = 'PENDING'
}
// ✓ Good: Const enums for performance
const enum Direction {
Up,
Down,
Left,
Right
}
// ✓ Alternative: Union types (more flexible)
type Status = 'active' | 'inactive' | 'pending'
// ✓ Alternative: Object with as const
const Status = {
Active: 'ACTIVE',
Inactive: 'INACTIVE',
Pending: 'PENDING'
} as const
type StatusType = typeof Status[keyof typeof Status]
🔹 Error Handling
Handle errors properly with types:
// ✓ Good: Custom error classes
class ValidationError extends Error {
constructor(message: string) {
super(message)
this.name = 'ValidationError'
}
}
// ✓ Good: Result type pattern
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E }
function parseUser(json: string): Result<User> {
try {
const data = JSON.parse(json)
return { success: true, data }
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Unknown error')
}
}
}
// Usage
const result = parseUser(jsonString)
if (result.success) {
console.log(result.data.name)
} else {
console.error(result.error.message)
}