I was a JavaScript developer who avoided TypeScript for too long. When I finally committed to it, I spent months learning patterns the hard way. These are the ones I wish someone had shown me from the start.
1. Use type for Unions, interface for Objects
The debate between type and interface goes on forever. Here's the simple rule I follow:
// Use `interface` for objects you might extend
interface User {
id: string
name: string
email: string
}
interface AdminUser extends User {
permissions: string[]
}
// Use `type` for unions, intersections, and aliases
type Status = 'active' | 'inactive' | 'pending'
type ID = string | number
type AdminOrUser = AdminUser | User2. Discriminated Unions are Underrated
This pattern eliminates entire categories of runtime errors:
type ApiResponse<T> =
| { success: true; data: T }
| { success: false; error: string }
async function fetchUser(id: string): Promise<ApiResponse<User>> {
try {
const user = await db.users.findById(id)
return { success: true, data: user }
} catch (err) {
return { success: false, error: 'User not found' }
}
}
// Now TypeScript knows exactly what's available
const result = await fetchUser('123')
if (result.success) {
console.log(result.data.name) // TypeScript knows `data` exists
} else {
console.log(result.error) // TypeScript knows `error` exists
}3. as const for Read-only Tuples and Objects
// Without `as const` — TypeScript infers broad types
const SIZES = ['sm', 'md', 'lg']
// Type: string[] ← useless for autocomplete
// With `as const` — TypeScript infers exact literal types
const SIZES = ['sm', 'md', 'lg'] as const
// Type: readonly ["sm", "md", "lg"] ← much better!
type Size = typeof SIZES[number] // "sm" | "md" | "lg"
function getSize(size: Size) { /* ... */ }
getSize('xl') // TypeScript error! ✓4. Utility Types You Should Know
// Pick — select specific properties
type UserPreview = Pick<User, 'id' | 'name'>
// Omit — exclude specific properties
type CreateUserInput = Omit<User, 'id' | 'createdAt'>
// Partial — make all properties optional
type UpdateUserInput = Partial<User>
// Required — make all properties required
type FullUser = Required<User>
// Record — typed key-value pairs
type UserMap = Record<string, User>
// ReturnType — extract function return type
async function getUser() { /* ... */ return user }
type GetUserReturn = Awaited<ReturnType<typeof getUser>>5. Generic Constraints
Don't use any. Use generics with constraints instead:
// ❌ Any destroys type safety
function first(arr: any[]): any {
return arr[0]
}
// ✓ Generic preserves type information
function first<T>(arr: T[]): T | undefined {
return arr[0]
}
const num = first([1, 2, 3]) // type: number | undefined
const str = first(['a', 'b']) // type: string | undefined
// With constraints — T must have an `id` property
function findById<T extends { id: string }>(
items: T[],
id: string
): T | undefined {
return items.find(item => item.id === id)
}6. Type Guards
// Custom type guard
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value
)
}
// Usage
const data: unknown = await fetch('/api/user').then(r => r.json())
if (isUser(data)) {
console.log(data.name) // TypeScript knows this is a User
}7. Don't Over-type — Let TypeScript Infer
One of my early mistakes was annotating everything:
// ❌ Unnecessary — TypeScript already knows the type
const name: string = 'Solomon Akor'
const count: number = 42
const isActive: boolean = true
// ✓ Let TypeScript infer
const name = 'Solomon Akor' // TypeScript: string
const count = 42 // TypeScript: number
const isActive = true // TypeScript: booleanAnnotate when:
- TypeScript can't infer (function parameters)
- You want a wider/narrower type than what TypeScript infers
- The inferred type is wrong
The Bottom Line
TypeScript's value is proportional to how seriously you take it. Casting to any everywhere defeats the purpose. The patterns above — especially discriminated unions and utility types — will make your code dramatically safer with minimal friction.
Start with strict: true in your tsconfig.json and fix the errors. You'll write better JavaScript from day one.
