DEV Community

Cover image for TypeScript Type System Tricks That Changed How I Code
Monalisa Das
Monalisa Das

Posted on Originally published at monalisadas-knowme.vercel.app

TypeScript Type System Tricks That Changed How I Code

Once you understand TypeScript's type system as a programming language in its own right, everything changes. Here are the patterns that made me a better TypeScript developer.

Most TypeScript developers use maybe 20% of the type system. They annotate functions, use interfaces, maybe throw in a generic here and there. But TypeScript's type system is genuinely Turing-complete — you can do computation at the type level. Here's what that unlocks.

Template Literal Types

Template literal types let you construct new string types by combining existing ones. This is incredibly useful for building type-safe event systems, CSS-in-JS APIs, and routing.

type EventName = 'click' | 'focus' | 'blur'
// Capitalize is a built-in TypeScript utility -- turns 'click' into 'Click'
type Handler = `on${Capitalize<EventName>}` // 'onClick' | 'onFocus' | 'onBlur'

// Here's why this matters: the compiler knows every valid prop name
// You can't pass 'onDoubleclick' -- it won't compile
type Side = 'top' | 'right' | 'bottom' | 'left'
type Spacing = `margin-${Side}` | `padding-${Side}`
// 'margin-top' | 'margin-right' | ... | 'padding-left'
// All 8 strings, derived from 2 unions -- no manual list
Enter fullscreen mode Exit fullscreen mode

Discriminated Unions with Exhaustive Checks

The most underused TypeScript pattern. Model your state as a discriminated union and let the compiler tell you when you've forgotten a case.

// Every possible state is explicit -- no boolean flags that combine illegally
type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }   // data only exists in success
  | { status: 'error'; error: Error } // error only exists in error

// assertNever: if TypeScript lets you reach this line, you missed a case
// It's a compile-time guarantee, not a runtime one
function assertNever(x: never): never {
  throw new Error('Unexpected value: ' + x)
}

function render<T>(state: AsyncState<T>) {
  switch (state.status) {
    case 'idle':    return <Idle />
    case 'loading': return <Spinner />
    case 'success': return <Data value={state.data} /> // state.data is T here, not T | undefined
    case 'error':   return <ErrorView error={state.error} />
    default:        return assertNever(state) // Remove a case above -- this line turns red
  }
}
Enter fullscreen mode Exit fullscreen mode

Infer in Conditional Types

The infer keyword lets you extract parts of a type within a conditional. This powers utilities like ReturnType, Awaited, and Parameters — and you can build your own.

// 'infer R' means: if T matches Promise<something>, capture that something as R
type Awaited<T> = T extends Promise<infer R> ? R : T

// Same idea -- capture the element type out of an array
type ElementOf<T> = T extends Array<infer E> ? E : never

// Capture just the first argument, ignore the rest
type FirstArg<F> = F extends (first: infer A, ...rest: any[]) => any ? A : never

type A = FirstArg<(name: string, age: number) => void> // string
// These aren't magic -- the compiler checks the shape,
// and infer gives you a name to use on the right-hand side
Enter fullscreen mode Exit fullscreen mode

Run the demos yourself — template literals, discriminated unions, and infer all in one TypeScript file

These three patterns cover 80% of what 'advanced TypeScript' actually means in practice. Template literals for string type composition. Discriminated unions for modelling state correctly. Infer for writing your own type utilities. Once you're comfortable with them, you stop fighting the type system and start using it as a design tool.

Top comments (0)