TypeScript Tips and Tricks for Better Code
TypeScript JavaScript Programming Tips

TypeScript Tips and Tricks for Better Code

Level up your TypeScript skills with these practical tips and advanced patterns. Learn utility types, generic constraints, and best practices for type-safe development.

Francisco Pessano
6 min read

TypeScript Tips and Tricks for Better Code

TypeScript has become an essential tool for JavaScript developers. Here are some practical tips and advanced patterns that will help you write better, more type-safe code.

Utility Types

TypeScript comes with many built-in utility types that can save you time and make your code more expressive.

Pick and Omit

Extract or exclude properties from existing types:

interface User {
  id: string
  name: string
  email: string
  password: string
  createdAt: Date
}

// Create a public user type without sensitive data
type PublicUser = Omit<User, 'password'>

// Create a type for user creation
type CreateUser = Pick<User, 'name' | 'email' | 'password'>

Partial and Required

Make properties optional or required:

// Make all properties optional for updates
type UpdateUser = Partial<User>

// Make specific properties required
type UserWithRequiredEmail = Required<Pick<User, 'email'>> & Partial<User>

Generic Constraints

Use constraints to make your generics more specific:

// Constraint to objects with an id property
interface HasId {
  id: string
}

function updateEntity<T extends HasId>(entity: T, updates: Partial<T>): T {
  return { ...entity, ...updates }
}

// This ensures you can only update entities that have an id
const updatedUser = updateEntity(user, { name: 'New Name' })

Discriminated Unions

Create type-safe state machines:

type LoadingState = {
  status: 'loading'
}

type SuccessState = {
  status: 'success'
  data: any
}

type ErrorState = {
  status: 'error'
  error: string
}

type AsyncState = LoadingState | SuccessState | ErrorState

function handleState(state: AsyncState) {
  switch (state.status) {
    case 'loading':
      // TypeScript knows there's no data or error here
      return 'Loading...'
    case 'success':
      // TypeScript knows data is available
      return state.data
    case 'error':
      // TypeScript knows error is available
      return `Error: ${state.error}`
  }
}

Mapped Types

Transform existing types programmatically:

// Make all properties readonly
type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}

// Make all properties optional
type Optional<T> = {
  [P in keyof T]?: T[P]
}

// Create event handlers for form fields
type FormHandlers<T> = {
  [K in keyof T as `handle${Capitalize<string & K>}Change`]: (value: T[K]) => void
}

type UserFormHandlers = FormHandlers<User>
// Results in:
// {
//   handleIdChange: (value: string) => void
//   handleNameChange: (value: string) => void
//   handleEmailChange: (value: string) => void
// }

Template Literal Types

Create precise string types:

type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type Version = 'v1' | 'v2'
type APIEndpoint = `/${Version}/${string}`

// Type-safe API route function
function apiCall<T>(method: HTTPMethod, endpoint: APIEndpoint): Promise<T> {
  // Implementation
}

// Usage - TypeScript ensures the endpoint format is correct
apiCall('GET', '/v1/users') // ✅ Valid
apiCall('GET', '/users')    // ❌ Invalid - missing version

Conditional Types

Create types that depend on conditions:

type ApiResponse<T> = T extends string
  ? { message: T }
  : T extends object
  ? { data: T }
  : never

type StringResponse = ApiResponse<string>    // { message: string }
type ObjectResponse = ApiResponse<User>      // { data: User }

Best Practices

1. Use unknown instead of any

// Bad
function parseJSON(json: string): any {
  return JSON.parse(json)
}

// Good
function parseJSON(json: string): unknown {
  return JSON.parse(json)
}

2. Prefer const assertions

// Creates a more specific type
const colors = ['red', 'green', 'blue'] as const
type Color = typeof colors[number] // 'red' | 'green' | 'blue'

3. Use type predicates for runtime checks

function isString(value: unknown): value is string {
  return typeof value === 'string'
}

// Now TypeScript knows the type after the check
if (isString(userInput)) {
  // userInput is definitely a string here
  console.log(userInput.toUpperCase())
}

Conclusion

TypeScript’s type system is incredibly powerful. These patterns and utilities help you write more expressive, safer code while maintaining the flexibility that makes JavaScript great.

The key is to start simple and gradually adopt more advanced patterns as your needs grow. Your future self (and your teammates) will thank you for the extra type safety!