Back to Blog
X

TypeScript: React Patterns That Scale in Production

In this article

TypeScript is not about adding types until the compiler stops complaining. In medium and large React projects, the goal is to prevent bugs and make clear what each layer expects.

After years of React in production (AEM, LGPD compliance, own products), these patterns help most when the team grows.


1. Model the domain before the component

Instead of data: any, define types near the hook or service:

ts
export type PricePoint = {
  date: string
  value: number
  marketplace: 'amazon' | 'mercadolivre' | 'shopee'
}

export type ProductSummary = {
  id: string
  title: string
  currentPrice: number
  history: PricePoint[]
}

The component receives ProductSummary — not loose API JSON. If the backend changes, the error shows up in the right place.


2. Use unions for UI state

Loading, error, empty, and success are safer with a discriminated union:

ts
type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'error'; message: string }
  | { status: 'success'; data: T }

In JSX, a switch with never in default forces handling every case — and avoids showing data while still loading.


3. Clear props, not many booleans

Prefer a named variant over five true/false flags:

ts
// ❌ hard to maintain
type ButtonProps = { primary?: boolean; danger?: boolean; ghost?: boolean }

// ✅ clear and extensible
type ButtonProps = {
  variant?: 'primary' | 'secondary' | 'ghost' | 'danger'
  size?: 'sm' | 'md' | 'lg'
}

In design systems, this avoids invalid combos (primary + ghost at once).


4. satisfies and as const for configs

Menus, routes, and feature flags stay typed without losing literal values:

ts
const NAV_ITEMS = [
  { href: 'expertise', key: 'expertise' },
  { href: 'projects', key: 'projects' },
] as const satisfies ReadonlyArray<{ href: string; key: string }>

You keep autocomplete and validation at the definition.


5. Validation at the API boundary (Zod or similar)

TypeScript disappears after build. Runtime validation at the API entry protects the UI:

ts
const ProductSchema = z.object({
  id: z.string(),
  title: z.string(),
  currentPrice: z.number().nonnegative(),
})

const product = ProductSchema.parse(await response.json())

Especially important with third-party APIs or scraping, where payloads can change without notice.


Conclusion

TypeScript scales when types describe the contract between layers — not when they decorate JSX.

Start with the domain, model async state with unions, simplify props, and validate at the boundary. The team ships faster with more deploy confidence.

Glauber Ariel Magalhães

Written by Glauber Ariel Magalhães

Senior software engineer. I write about React, performance, accessibility, and security — with examples from real projects.

Comments

Sign in with your GitHub account to comment. Comments are public, stored as Discussions on the blog's comments repository.