Back to Blog

TypeScript Tips That Actually Matter for Beginners

Practical TypeScript patterns I use every day, beyond the basics. No theory, just things that make your code safer and your IDE more helpful.

Solomon AkorApril 5, 20244 min read

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 | User

2. 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: boolean

Annotate 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.

Share:
Solomon Akor

Solomon Akor

Founder & Lead Engineer, AkorLabs Technologies

Computer Science graduate building modern web applications and leading industrial operations across Nigeria. Writing about tech, business, and the weighing industry.

About the author →

Related Articles