Introduction
React is amazing, but let’s face it, as the application grows you can find yourself going back and forth, changing files, just because you don’t remember how you called a prop. It just adds more chaos into the freedom that JavaScript offers, which often lead to bugs. That makes it imperative to use TypeScript. The adoption of static typing can help us catch errors in advance, prevent bugs, improve code maintainability and in the long term enhance productivity we provide to the developer the best experience.
I get it, it gets hard at first, that’s why I created this quick cheatsheet, so you can catch up with the best static typing practices using React. It will provide you with a quick reference for commonly used patterns, syntax, and best practices when working with React and TypeScript.
Prop types
Let’s start with the one of the basics the props.
- In the child we define what is going to be the type for the props
- Use type instead of interface to make it reusable in other files
export type CardProps = {
className: string;
};
const Card = (props: CardProps) => {
return <div className={props.className}>Card content</div>;
};
const Parent = () => {
return <Card className="class"></Card>;
};
Another version destructuring the props inline:
export type CardProps = {
className: string;
};
const Card = ({ className }: CardProps) => {
return <div className={className}>Card content</div>;
};
const Parent = () => {
return <Card className="class"></Card>;
};
And finally we can even define the types inline as well
const Card = ({ className }: {className: string}) => {
return <div className={className}>Card content</div>;
};
const Parent = () => {
return <Card className="class"></Card>;
};
Children types
Usually the easiest way to get this works (and tempting) is use any
in the children definition, but is just use the type React.ReactNode
And like before we can use the inline types as well if you prefer it.
export type CardProps = {
children: React.ReactNode;
};
const Card = (props: CardProps) => {
return <div>{props.children}</div>;
};
const Parent = () => {
return <Card>hi there</Card>;
};
Event handler types
Here we need to start using inference from TypeScript, usually this is going to be the types that we are going to use to define the props. Let’s take a look to the following example.
export type CardProps = {
className: string;
children: React.ReactNode;
onClick: React.MouseEventHandler<HTMLButtonElement>;
};
const Card = ({ children, className, onClick }: CardProps) => {
return (
<button className={className} onClick={onClick}>
{children}
</button>
);
};
const Parent = () => {
const onClick = () => {
console.log('hi there')
}
return (
<Card className="my class" onClick={onClick}>
hi there
</Card>
);
};
We have a click event and If we hover over it we’ll se a menu displaying the type, we can take it and insert in the definition of the props.
Lift events
What if I want to pass events? Let’s look first at the definition of the props.
Now the props is going to become a function, this function will have an event as input and return a void function. We can type this event, otherwise It’ll complain. To solve it just copy the last type definition and remove the Handler because now we are defining the event and not the handler.
//step 1
export type CardProps = {
onClick: (event) => void;
};
//step 2
export type CardProps = {
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
};
Now in the parent the event type will be lifted automatically and we can use the hover to see the definition of it again, and type it in the function.
Lift values
We might have the need to pass some value from the children to the parent as well. We can include it in the definition of the props. In the children we have to pass to the function the event and also the input, in this case and id
and define it in the prop definition. Finally we’ll be able to see the type in the parent (hovering to the event) and include in our function.
export type CardProps = {
className: string;
children: React.ReactNode;
onClick: (event: React.MouseEvent<HTMLButtonElement>, id: number) => void;
};
const Card = ({ children, className, onClick }: CardProps) => {
return (
<div>
<button className={className} onClick={(event) => {onClick(event, 1)}}>
{children}
</button>
</div>
);
};
const Parent = () => {
const onClick = (e: React.MouseEvent<HTMLButtonElement>, id: number) => {
console.log("on click", e, id);
};
return (
<Card className="my class" onClick={onClick}>
hi there
</Card>
);
Use state
Looking at the following example we know for sure that the list
is going to be an array with objects, containing an id and value. What we can do is define the individual type and tell the state that list
is going to be an array of that type.
// step 1
const Parent = () => {
const [list, setList] = useState([]);
return (
<div>
{list.map((element) => {
return <div key={element.id}>{element.value}</div>;
})}
</div>
);
};
// step 2
export type List = {
id: number;
value: string;
};
const Parent = () => {
const [list, setList] = useState<List[]>([]);
return (
<div>
{list.map((element) => {
return <div key={element.id}>{element.value}</div>;
})}
</div>
);
};
Use reducer
It should be easy to type the state as number but the action it’s a little tricky, we can have both add
and subtract
and place an option in the type is not going to be possible.
const reducer = (state: unknown, action: unknown) => {
switch (action.type) {
case "add":
return { count: state.count + action.add };
case "subtract":
return { count: state.count - action.subtract };
default:
throw new Error();
}
};
const [state, dispatch] = useReducer(reducer, { count: 0 });
dispatch({ type: "add", add: 1 });
What it can be done is place optional type for the action reducer and add it to the definition of the reducer.
type ReducerState = {
count: number;
};
type ReducerAction =
| {
type: "add";
add: number;
}
| {
type: "subtract";
subtract: number;
};
const reducer = (state: ReducerState, action: ReducerAction) => {
switch (action.type) {
case "add":
return { count: state.count + action.add };
case "subtract":
return { count: state.count - action.subtract };
default:
throw new Error();
}
};
const [state, dispatch] = useReducer(reducer, { count: 0 });
dispatch({ type: "add", add: 1 });
Resources
If you want to study more about how to correctly type your React code, you can also visit React Typescript Cheatsheet which I find quite useful.
Wrap up
Mastering TypeScript with React can be challenging sometimes, but once you’ve got the hang of it, it can significantly improve your development skills. It helps prevent bugs, enhances code maintainability, and boosts productivity.
In this guide, we’ve covered some commonly used patterns, syntax, and best practices with examples for prop types, children types, event handlers, lifted prop events and inputs, and the usage of several hooks such as useState, useCallback, useMemo, and useReducer. I hope you found this quick cheatsheet useful. As always, practice is key to mastering, so keep coding!
There’s a lot more to explore, and I encourage you to dig deeper into the topic and learn more about the endless possibilities TypeScript offers when used in conjunction with React.
Happy coding!