React + TypeScript Design Guide
Start simple
Today, it is easy to create a React + TypeScript project with a Vite template.
npm create vite@latest my-app -- --template react-ts
At the start, what matters is less the setup itself and more the rules the team will share: enable strict, minimize any, and separate domain models from UI models.
Prop types are the component contract
Prop types act as documentation for reuse. It helps to separate required values from optional ones clearly and use names that make intent obvious.
interface ButtonProps {
variant?: 'primary' | 'secondary'
disabled?: boolean
onClick?: () => void
children: React.ReactNode
}
function Button({ variant = 'primary', disabled = false, onClick, children }: ButtonProps) {
return (
<button className={variant} disabled={disabled} onClick={onClick}>
{children}
</button>
)
}
Unions are stronger than stringly state
If loading, success, and failure are modeled with several booleans, it becomes easy to represent invalid combinations. In TypeScript, it is safer to model branching state with a union.
type FetchState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; message: string }
This works well with render branches and reduces impossible state combinations.
Be explicit about event types only where it helps
Beginners often try to write every event type by hand. In real projects, it is usually better to lean on IDE inference. But handlers that are extracted or reused tend to read more clearly with explicit types.
Use generics only at real reuse boundaries
Once you have TypeScript, it is tempting to build generic components early. In practice, it is better to wait until a pattern repeats at least two or three times. Generalizing too early often hurts usability.
Common mistakes
One of the most common mistakes is spreading backend DTOs directly through UI components. When the API response shape changes, the entire screen layer shakes. Another common issue is overusing React.FC or escaping with any, which weakens trust in the type system.
Wrap-up
The core of React + TypeScript is not advanced type wizardry, but clear contracts. When props, state, events, and data models are separated appropriately, types stop being a burden and become interfaces that make the codebase more resilient to change.
What Gets Hard in Production
- Type drift starts when API DTOs, form models, and view props all reuse the same interface.
- Compile-time safety drops quickly if the team treats
anyas a shortcut instead of a last resort. - Shared utility types become a maintenance burden when they hide business meaning behind clever abstractions.
Architecture Decisions That Matter
- Turn on
strict,noUncheckedIndexedAccess, and path alias rules early so bad patterns do not become default. - Separate domain types, server response types, and UI state types instead of forcing one universal model.
- Prefer explicit module boundaries over a giant
types.tsfile that every screen imports.
Practical Example
A stable pattern is to map transport types into UI-friendly models at the boundary:
type UserResponse = {
id: string
full_name: string
last_login_at: string | null
}
type UserCardModel = {
id: string
displayName: string
isDormant: boolean
}
function toUserCardModel(user: UserResponse): UserCardModel {
return {
id: user.id,
displayName: user.full_name,
isDormant: user.last_login_at === null,
}
}
Anti-Patterns to Avoid
- Exporting backend response shapes directly into reusable components.
- Building generic helpers before the repetition is real and measured.
- Using type assertions to silence warnings that are actually modeling problems.
Operational Checklist
- Review
tsconfigchanges as architecture decisions, not formatting trivia. - Keep ESLint rules aligned with the TypeScript strictness level.
- Require explicit typing at public boundaries such as hooks, context values, and component props.
- Watch incremental build time as the type graph grows.
Final Judgment
React and TypeScript become a flagship combination when types describe real boundaries. If types only mirror implementation details, the team pays the complexity cost without gaining safety.
Continue Reading
Related posts
Reconciliation Boundaries in Optimistic UI
Optimistic UI feels fast, but complexity appears when the server disagrees. This guide explains where optimistic updates should stop.
🖥️ FrontendReact Activity and View Transitions Adoption Notes
Recent React work around Activity and View Transitions is about more than animation. It changes how teams can approach UI continuity and preserved state.
📈 TrendsWhat the React Foundation Means for Engineering Teams
Why the React Foundation matters beyond governance news, and how it may affect framework coordination, ecosystem stewardship, and long-term frontend strategy.
💬 LanguageTypeScript Utility Types: A Practical Guide
A production-focused guide to TypeScript utility types. Learn how to model DTOs, update payloads, selectors, and derived types without making your type layer harder to read.
Next Path