TypeScript Enterprise Architecture: Generics, Strict Typing & Utility Types
What You Will Master in This Tutorial
- Design type-safe API client wrappers using advanced generic parameters and return inferences
- Eliminate runtime bugs using strict discriminated unions and exhaustive exhaustiveness checking
- Leverage TypeScript 5.5+ inferred type predicates to sanitize incoming untrusted JSON data
- Master utility types: ReturnType, Parameters, Omit, Pick, and custom DeepReadonly wrappers
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function renderState<T>(state: AsyncState<T>): string {
switch (state.status) {
case 'idle':
return 'Awaiting input...';
case 'loading':
return 'Fetching payload from edge...';
case 'success':
return `Loaded payload: ${JSON.stringify(state.data)}`;
case 'error':
return `Failed with exception: ${state.error.message}`;
}
}
Frequently Asked Questions
Going deeper: generics and strict typing in practice
The pattern at the heart of this tutorial β a generic discriminated union like AsyncState<T> with an exhaustive switch β is how large TypeScript codebases stay refactorable. In practice, generics earn their keep in three places: API client wrappers that infer response types instead of casting them, state machines whose impossible states become unrepresentable, and boundaries where untrusted JSON enters the system, where TypeScript 5.5's inferred type predicates help sanitize data before it spreads. Utility types such as ReturnType, Parameters, Pick, Omit, and a custom DeepReadonly then keep every derived type in sync automatically. The discipline is restraint: each generic should remove a cast or a duplicated type, not decorate the code.
Common mistakes
- Reaching for any. Every any switches off the checker exactly where enterprise code needs it most; unknown plus narrowing keeps the guarantee and forces honest handling.
- Non-exhaustive switches. A switch over a union without a never check in the default branch compiles fine today and silently misbehaves when someone adds a new variant tomorrow.
- Trusting types at runtime. Types are erased at compile time, so a typed API response is only a claim; external data still needs runtime validation before the app relies on it.
- Over-engineered generics. Type gymnastics that nobody on the team can read are a liability; if a generic does not remove duplication or a cast, a concrete type is better.
Frequently asked questions
Do complex types slow down the running app?
No. As this tutorial's FAQ states, TypeScript types are completely erased during compilation, so runtime performance is purely that of the emitted JavaScript.
When should I model state as a discriminated union?
Whenever the states are mutually exclusive and carry different data β like idle, loading, success, and error β because the compiler then forces every consumer to handle each case.
What is an inferred type predicate in TypeScript 5.5?
It lets the compiler deduce that a filter function is also a type guard, so sanitizing untrusted JSON with a .filter call narrows the resulting array's type automatically β one of the techniques this tutorial uses to clean incoming data without manual type assertions.
What are utility types actually for?
They derive new types from existing ones β ReturnType and Parameters from functions, Pick and Omit from object shapes β so a change in one place propagates everywhere instead of drifting out of sync across the codebase over time.