Picovert

TypeScript Best Practices 2026: Patterns That Scale

2026-04-188 min read

TypeScript adoption is nearly universal in 2026. The interesting question is no longer "should we use TypeScript?" but "are we using it well?" This guide covers the patterns that prevent entire categories of runtime bugs, not just type errors.

Strict mode: the non-negotiable baseline

Enable all strict checks in tsconfig.json:

{"compilerOptions": {"strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true}}

  • strict: true enables strictNullChecks, noImplicitAny, and 7 others.
  • noUncheckedIndexedAccess adds undefined to array index results — catches arr[0].name crashes when the array is empty.
  • exactOptionalPropertyTypes prevents assigning undefined to an optional property when the property simply shouldn't exist.

Discriminated unions over boolean flags

Boolean flags create exponentially growing state combinations. A component with 3 booleans has 8 possible states — most invalid. Model state as discriminated unions instead:

// Bad: 8 state combinations, most are invalid type State = { isLoading: boolean; isError: boolean; data?: User }; // Good: exactly the valid states type State = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: User } | { status: 'error'; error: string };

TypeScript narrows the type inside a switch on status, ensuring you can't accidentally access data in the error state.

The satisfies operator

satisfies validates that a value conforms to a type while preserving the literal type for further inference:

const config = { routes: ['/home', '/about'] } satisfies Config; // config.routes is string[] (preserved), not Config['routes'] (widened)

Use it when you want type checking without losing the precision of literal inference.

Template literal types for safe string APIs

Template literal types let you constrain strings to a specific pattern:

type EventName = `on${Capitalize<string>}`; // 'onClick', 'onChange', etc. type CSSProperty = `${string}-${string}`; // 'margin-top', 'padding-left', etc.

Combined with mapped types, you can build safe event emitters, CSS-in-JS APIs, and routing utilities that enforce naming conventions at compile time.

Branded types for domain modeling

TypeScript's structural typing means two types with the same shape are interchangeable. Branded types add a unique nominal brand to prevent accidental interchange:

type UserId = string & { readonly __brand: 'UserId' }; type ProductId = string & { readonly __brand: 'ProductId' }; function getUser(id: UserId) { ... } getUser('abc' as UserId); // OK getUser(productId); // Type error!

This catches the class of bug where you pass a product ID where a user ID is expected — both are strings structurally, but semantically different.

const type parameters

The const modifier on type parameters infers literal types instead of widened types:

function createRoute<const T extends string>(path: T) { return path; } const route = createRoute('/users'); // type: '/users', not string

Avoid type assertions — use type guards

Type assertions (as SomeType) bypass the type checker entirely. Replace them with type guard functions that actually verify the type at runtime:

function isUser(value: unknown): value is User { return typeof value === 'object' && value !== null && 'id' in value && 'name' in value; }

A well-written type guard both narrows the TypeScript type and validates at runtime — essential at API boundaries where unknown data arrives.

Infer return types, don't annotate them

Explicit return type annotations on functions are redundant when TypeScript can infer them. They also create a maintenance burden — you annotate the return type, then the implementation changes, and the annotation becomes a lie that TypeScript enforces.

Exception: public API functions in library code, where the return type is part of the contract and should be enforced.