TypeScript + React: Tips & Tricks
The advantages of adding TypeScript
to React
have become impossible to ignore. Today, most React
projects choose to type their components and hooks to prevent errors, facilitate code refactoring, and generally improve the development experience.
In this post, I'll share some TypeScript + React recipes that I've collected over the last few years working with this stack.
Generic Functional Components
import { ReactNode, useState } from "react";
interface Props<T> {
item: T;
onClickRow: (item: T) => ReactNode;
}
const Row = <T,>(props: Props<T>) => {
const { item, onClickRow } = props;
return (
<div>
{JSON.stringify(item, null, 2)}
<button onClick={() => onClickRow(item)}>Click me</button>
</div>
);
};
Usage:
<Row<Address> item={address} onClick={onAddressClick} />
This component accepts any type of item
as a prop and calls the onClickRow
handler, whose parameter is of the same type as item
.
Note the <T, >
before the function definition. You can't simply use <T>
because the TSX
parser doesn't know if it's a JSX tag or a generic component declaration (Explanation here).
With this, you have a generic functional component that accepts a type and calculates the type of its props based on the type passed as a parameter when invoking the component.
Limited Generic Components
type RequireFieldsInRow = { date: string };
type Props<T> = {
row: T;
};
export const DateColumn = <T extends RequireFieldsInRow>({
row: { date },
}: Props<T>) => {
return <div>{formatDate(date)}</div>;
};
This is a generic component that renders a date column. The type it accepts as a parameter can be any object that extends RequireFieldsInRow
, that is, any object with a date
property of type string
.
In this way, we narrow the parameterizable type, limiting the types that are accepted.
Typing a ContextProvider
type State = { isDarkMode: boolean };
type MyContextType = {
state: State;
setState: React.Dispatch<React.SetStateAction<State>>;
};
const MyContext = React.createContext<MyContextType | undefined>(undefined);
function MyContextProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = React.useState({ isDarkMode: false });
const value = { state, setState };
return <MyContext.Provider value={value}>{children}</MyContext.Provider>;
}
Here we see how the call to createContext
does not carry default values, as we want the value to be initialized directly in the Provider
(Explanation here).
Typing children
type Props = {
children1: JSX.Element; // ❌ Bad, doesn't accept arrays
children2: JSX.Element | JSX.Element[]; // 🤔 Meh, doesn't accept strings
children3: React.ReactChildren; // ❌ Not a type, it's a utility
children4: React.ReactChild[]; // ✅ Better, accepts arrays
children: React.ReactNode; // ✅ BEST, accepts everything
style?: React.CSSProperties; // Extra: for passing style properties
};
React.ReactNode
is the best type, as it accepts any type of children
that React can render.
More details at: TypeScript + React Cheatsheet.
Typing props
type Props = {
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
onClick(event: React.MouseEvent<HTMLButtonElement>): void;
style?: React.CSSProperties;
};
Some recurring types in props: event handlers and style properties. More at: TypeScript + React Cheatsheet.
Asserting Object Properties
const { values } = useForm<CreateFormPayload | EditFormPayload>(…);
const isEdit = hasOwnProperty(values, 'id') && !!values.id;
export function hasOwnProperty<MyObject extends unknown, MyProperty extends PropertyKey>(
object: MyObject,
property: MyProperty,
): object is MyObject & Record<MyProperty, unknown> {
return Object.prototype.hasOwnProperty.call(object, property);
}
More details at: Typescript hasOwnProperty.
Repository of tsconfig
Bases
If you want to explore different tsconfig
configurations for different environments (node
, react
, react-native
, etc.), here's a good resource:
And that's all, my friend. 🚀