TypeScript Best Practices for Modern Web Development
Go beyond basic typing — master the TypeScript patterns that eliminate entire classes of bugs and make large codebases a joy to work in.
TypeScript adoption has exploded across the JavaScript ecosystem, and for good reason: it catches errors at compile time, provides rich IDE tooling, and serves as living documentation. But most developers only scratch the surface. These are the patterns that separate competent TypeScript from exceptional TypeScript.
Strict Mode Is Non-Negotiable
Always enable "strict": true in your tsconfig.json. This activates strictNullChecks, noImplicitAny, and several other checks that catch the most common runtime errors. Starting a project without strict mode and enabling it later is painful — do it from day one.
Discriminated Unions for Complex State
Instead of using boolean flags (isLoading, isError, isSuccess), model state as a discriminated union:
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string }
This makes impossible states unrepresentable. You can't accidentally access data when the status is error. The TypeScript compiler enforces correct handling of every case.
Utility Types That Save Time
TypeScript ships with powerful utility types that most developers underuse. Partial<T> makes all properties optional (perfect for update DTOs). Pick<T, K> and Omit<T, K> derive focused types from existing ones. ReturnType<F> extracts a function's return type — essential for typing Redux selectors or API response handlers.
Generics: Write Once, Type Everywhere
Generics are TypeScript's most powerful feature and the most underused. A well-typed API client, form hook, or data-fetching utility built with generics eliminates repetitive type assertions across your codebase. The key insight: generics let you write type-safe abstractions without sacrificing flexibility.
The satisfies Operator
Introduced in TypeScript 4.9, satisfies validates that a value matches a type while preserving its most specific inferred type. This is invaluable for configuration objects and data dictionaries where you want type checking but also need autocomplete on specific properties.
Type Assertions as a Code Smell
Every as SomeType assertion in your codebase is a potential runtime error waiting to happen. Treat them as code smells. Instead of asserting, use type guards (typeof, instanceof, or custom is functions) to narrow types safely. If you find yourself writing as any, stop and redesign the types.
TypeScript's value compounds over time. The codebase that seems over-typed today is the one that survives refactoring, onboarding new engineers, and scaling to hundreds of thousands of lines without collapsing under its own complexity.