Advanced TypeScript 5 Patterns for Bulletproof Application State
Discriminated unions, template literal types, const type parameters, and satisfying schema constraints.
1. Const Type Parameters for Precise Literal Inference
Before TypeScript 5, calling a generic function with an array or object literal widened the inferred type by default: `['admin', 'editor', 'viewer']` inferred as `string[]`, not the tuple of literal string types you actually wrote. To get literal inference you had to append `as const` at every call site, which is easy to forget and impossible to enforce — nothing stopped a teammate from calling your function without it and silently losing the precision your API depended on.
The `const` modifier on a type parameter (`function f<const T>(...)`) moves that behavior into the function's signature itself. Every call site now infers as if `as const` had been applied, without the caller needing to remember to write it. This matters most for APIs that build behavior off literal values — a routing table, a state machine's list of valid transitions, a permissions system enumerating exact role strings — where `string[]` is too loose to catch typos or invalid values at the call site.
The practical win is catching errors where they're introduced, not where they're used. A permissions array typed as `string[]` lets `adimn` slip through silently; the same array inferred as the literal tuple `readonly ['admin', 'editor', 'viewer']` via a const type parameter makes any code downstream that checks membership against known roles get full autocomplete and exhaustiveness checking, and a typo becomes a compile error instead of a runtime bug discovered in production.
2. Exhaustive Discriminated Unions
A discriminated union models a value that can be one of several distinct shapes, tagged by a common literal field — usually called `type`, `status`, or `kind`. Modeling application state this way (`{ status: 'loading' } | { status: 'success', data: T } | { status: 'error', error: string }`) eliminates an entire category of bugs that plague apps using loosely-typed flags like separate `isLoading`, `data`, and `error` booleans and fields that can, in principle, all be set inconsistently at once — loading and error true simultaneously, data present while status says error, and so on.
The real payoff comes from exhaustiveness checking in `switch` statements. When you switch on the discriminant field and handle every variant, TypeScript can prove no case was missed — and more importantly, it will error at compile time the moment someone adds a new variant to the union and forgets to handle it anywhere it's switched on. This is the mechanism that turns 'we forgot to handle the new state in three places' from a production incident into a red squiggle in the editor before the PR is even opened.
The idiomatic way to enforce exhaustiveness is a small `assertNever` helper placed in the `default` case of every switch over a discriminated union. Because the parameter type is `never`, TypeScript only allows this branch to type-check if every other case has already narrowed the union to nothing — meaning every variant was actually handled. Skip this pattern and TypeScript will happily let a `default: break;` silently swallow new, unhandled cases forever.

