Back to Blog

5 TypeScript Tips I Wish I Knew Earlier

Practical TypeScript patterns and techniques that will level up your code quality and developer experience.

TypeScriptJavaScriptTips

After years of writing TypeScript, here are the patterns I find myself using most often. These aren't the basics you'll find in every tutorial—they're the practical techniques that make a real difference.

1. Const Assertions for Literal Types

Instead of getting string[], get the exact literal types:

// Without const assertion
const colors = ['red', 'green', 'blue']; // string[]

// With const assertion
const colors = ['red', 'green', 'blue'] as const; // readonly ["red", "green", "blue"]

// Now you can derive a union type
type Color = typeof colors[number]; // "red" | "green" | "blue"

2. Discriminated Unions for State Management

Perfect for representing different states:

type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

function handleState(state: AsyncState<User>) {
  switch (state.status) {
    case 'idle':
      return 'Click to load';
    case 'loading':
      return 'Loading...';
    case 'success':
      return `Hello, ${state.data.name}!`; // TypeScript knows data exists
    case 'error':
      return `Error: ${state.error.message}`; // TypeScript knows error exists
  }
}

3. Template Literal Types

Create types from string patterns:

type EventName = `on${Capitalize<'click' | 'hover' | 'focus'>}`;
// "onClick" | "onHover" | "onFocus"

type CSSProperty = `${number}${'px' | 'rem' | 'em' | '%'}`;
// Matches "10px", "1.5rem", "100%", etc.

4. The satisfies Operator

Get type checking without losing literal types:

// With type annotation: loses literal types
const config: Record<string, string> = {
  apiUrl: 'https://api.example.com',
  env: 'production',
};
config.apiUrl; // string (not the literal)

// With satisfies: keeps literal types
const config = {
  apiUrl: 'https://api.example.com',
  env: 'production',
} satisfies Record<string, string>;
config.apiUrl; // "https://api.example.com"

5. Branded Types for Type Safety

Prevent mixing up similar types:

type UserId = string & { readonly brand: unique symbol };
type OrderId = string & { readonly brand: unique symbol };

function createUserId(id: string): UserId {
  return id as UserId;
}

function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ }

const userId = createUserId('user_123');
getUser(userId); // ✅ Works
getOrder(userId); // ❌ Error: UserId is not assignable to OrderId

Conclusion

These patterns have significantly improved my TypeScript code. The key is understanding that TypeScript's type system is incredibly powerful—often the best solution involves leveraging the type system rather than working around it.

What TypeScript patterns do you find most useful? Let me know!