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, these are the patterns I reach for most. They sit a step past what the intro tutorials cover, and they make a real difference in day-to-day code.

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. TypeScript's type system is incredibly powerful, and the best solution is usually to work with it instead of around it.

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