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:
tsexport 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:
tstype 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:
tsconst 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:
tsconst 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.




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