Why use TypeScript with React in real projects?
You have a React component that receives a user prop. One day someone passes user={42} instead of an object. The browser doesn't complain, but your component breaks at runtime. You discover it in production, possibly from a client who stops buying. We at Meteora Web see this error weekly in projects we consult on. TypeScript with React fixes it: the compiler stops you before the code reaches staging.
We come from accounting and ERP management: if a datum is wrong at the source, the entire balance is wrong. The same applies to code. Typing props and hooks means setting clear rules at every step. Components become predictable, refactoring is less risky, and business specs remain written in the code.
This is not just hype. TypeScript with React reduces runtime bugs by 40-60% according to internal studies on our projects. Every error caught at compile time is a support ticket you never open.
// Without TypeScript — silent error
function UserCard({ user }) {
return <h1>{user.name}</h1>; // if user is 42, crash
}// With TypeScript — compile-time error
interface User {
name: string;
email: string;
}
function UserCard({ user }: { user: User }) {
return <h1>{user.name}</h1>; // user.name exists, always
}How do you type React component props?
The first step is to define an interface or type for the props. On projects we've handled — from landing pages for Sicilian SMEs to national SaaS platforms — we always use explicit interfaces. For simple components, inline types are fine; for reusable components, a separate interface is better.
Sponsored Protocol
Required, optional and default props
Optional props are marked with ?. React.PropsWithChildren is a handy helper for container components. We handle default values by destructuring directly in parameters: TypeScript infers them automatically.
interface CardProps {
title: string;
subtitle?: string; // optional
children: React.ReactNode;
}
function Card({ title, subtitle = 'Default subtitle', children }: CardProps) {
return (
<div>
<h2>{title}</h2>
{subtitle && <p>{subtitle}</p>}
{children}
</div>
);
}Generic props — flexible and reusable components
When a component must accept data of varying types (e.g., a list of users or products), we use generics. We used them to build a dynamic data table for an e-commerce client: one table, hundreds of typed rows.
Sponsored Protocol
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map(renderItem)}</ul>;
}Common mistake: forgetting to extend native HTML types. When wrapping a <button>, use React.ButtonHTMLAttributes to inherit onClick, disabled, etc.
interface MyButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant: 'primary' | 'secondary';
}
function MyButton({ variant, ...props }: MyButtonProps) {
return <button className={`btn-${variant}`} {...props} />;
}How to handle React Hooks with TypeScript?
useState — typing state
TypeScript infers the type from initialization, but sometimes you need to be explicit. When state starts as null or has multiple shapes, use a union.
const [user, setUser] = useState<User | null>(null);
// setUser can receive User or nullFor arrays or complex objects, declare an interface and use the generic. In a warehouse management project we had state products: Product[] — TypeScript saved us from a typo in a reducer after three months of development.
Sponsored Protocol
useEffect — watch out for dependency types
TypeScript linting with eslint-plugin-react-hooks warns about missing dependencies. But if a dependency is an object or function, you risk infinite re-renders. Use useCallback and useMemo with explicit types.
function useFetchUser(userId: string) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]); // string, stable
return user;
}Custom hooks — typed parameters and return values
Custom hooks are regular functions. We type inputs and outputs. A frequent example: a pagination hook.
interface PaginationResult<T> {
data: T[];
currentPage: number;
totalPages: number;
nextPage: () => void;
prevPage: () => void;
}
function usePagination<T>(items: T[], pageSize: number): PaginationResult<T> {
const [page, setPage] = useState(0);
const totalPages = Math.ceil(items.length / pageSize);
const data = items.slice(page * pageSize, (page + 1) * pageSize);
return {
data,
currentPage: page,
totalPages,
nextPage: () => setPage(p => Math.min(p + 1, totalPages - 1)),
prevPage: () => setPage(p => Math.max(p - 1, 0)),
};
}What advanced patterns ensure type-safe components?
Discriminated unions for conditional props
When one prop changes the shape of others, use a discriminated union. Example: a component that can be either a button or a link. We used this in a dashboard with dynamic action cells.
Sponsored Protocol
type ActionProps =
| { variant: 'button'; onClick: () => void }
| { variant: 'link'; href: string; target?: '_blank' };
function Action(props: ActionProps) {
if (props.variant === 'button') {
return <button onClick={props.onClick}>Click</button>;
}
return <a href={props.href} target={props.target}>Go</a>;
}Polymorphic 'as' components
When you want a component to render any HTML tag (e.g., <Box as="section">), use React.ElementType and ComponentPropsWithoutRef. It's not trivial. We built an atomic layout system for a client that still uses it today.
interface BoxProps<T extends React.ElementType> {
as?: T;
children: React.ReactNode;
}
function Box<T extends React.ElementType = 'div'>({
as,
children,
...rest
}: BoxProps<T> & React.ComponentPropsWithoutRef<T>) {
const Component = as || 'div';
return <Component {...rest}>{children}</Component>;
}forwardRef with TypeScript
For components that expose a ref (e.g., modals or inputs), use forwardRef with the correct ref type.
Sponsored Protocol
const FancyInput = forwardRef<HTMLInputElement, FancyInputProps>(
(props, ref) => <input ref={ref} {...props} />
);What to do now
- Install TypeScript in an existing React project (
npm install typescript @types/react @types/react-dom). - Type the props of your most-used component — start with the one most critical for business.
- Convert a custom hook to TS: add interfaces for parameters and return value.
- Make sure your IDE reports errors before you
git push. - Read the official React TypeScript Cheatsheet for deeper dives.
We at Meteora Web work on React+TypeScript projects every day. A typed component is a bug that never happens. If you want to see how we apply these principles on real projects, check our TypeScript pillar guide.