Typing props and events
Where types belong in day-to-day React – and where they don't
In React you write types in about three places: props, extracted event handlers and a few hooks. TypeScript works out the rest.
Props: one object, one type
A ComponentA function that describes what a piece of the interface should look like – a button, or a whole page.It always starts with a capital letter and returns JSX. You then use it like your own HTML element: <MyButton />.→ receives one object. So you describe exactly that object – nothing more.
type ButtonProps = {
label: string;
variant?: "primary" | "ghost"; // ? = optional
onClick: () => void; // function, no arguments, no return
};
export function Button({ label, variant = "primary", onClick }: ButtonProps) {
return <button className={variant} onClick={onClick}>{label}</button>;
} People used to write const X: React.FC<Props> = …. Today the plain function with a typed parameter is the standard – shorter and without surprises.
children: everything written in between
import type { ReactNode } from "react";
type CardProps = {
title: string;
children: ReactNode; // anything displayable: JSX, text, number, array, null
};
export function Card({ title, children }: CardProps) {
return (
<section>
<h2>{title}</h2>
{children}
</section>
);
}
// Usage:
<Card title="Notice">This ends up in children.</Card> ReactNode is nearly always right. JSX.Element is narrower – with it even <Card>Hello</Card> would be an error, because plain text is not an element.
Events: think from the element
// Written inline: TypeScript knows the type by itself – preferred!
<input onChange={(e) => console.log(e.target.value)} />
// Extracted: now you have to write the type yourself
import type { ChangeEvent, FormEvent, MouseEvent } from "react";
function handleChange(e: ChangeEvent<HTMLInputElement>) {
console.log(e.target.value);
}
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
}
function handleClick(e: MouseEvent<HTMLButtonElement>) {
console.log("clicked");
} Write the handler inline first and let the editor show you the type. Then pull it out and copy the type across – faster than guessing it.
Hooks: only where needed
// Usually the inferred type is enough:
const [count, setCount] = useState(0); // number
const [name, setName] = useState(""); // string
// Needed as soon as the starting value doesn't cover everything:
const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<string[]>([]);
// For a ref pointing at the page:
const inputRef = useRef<HTMLInputElement>(null); From an empty array TypeScript cannot tell what should be inside. After that every setItems(["a"]) is an error. Spell it out: useState<string[]>([]).
- +
Props – that is your component's contract
- +
Extracted event handlers
- +
useStatestarting with an empty array ornull - +
Anything coming from an API or a library
- −
Return types of components (
: JSX.Element) – inferred anyway - −
Local variables that are obvious
- −
anyas a stopgap
▸ In more depth: inheriting props and flexible building blocks optional
Take native attributes instead of retyping them
Your <Button> should also accept disabled, title, aria-label and the rest? You do not have to list them all.
type Props = {
label: string;
onClick: () => void;
disabled?: boolean;
type?: "button" | "submit";
className?: string;
// … and the other 40?
}; import type { ComponentProps } from "react";
type Props = ComponentProps<"button"> & {
label: string;
};
export function Button({ label, ...rest }: Props) {
return <button {...rest}>{label}</button>;
} ComponentProps<"button"> gives you every attribute a real <button> accepts. It works for your own components too: ComponentProps<typeof Card>.
One component for any kind of data
type ListProps<T> = {
items: T[];
show: (item: T) => ReactNode;
keyOf: (item: T) => string;
};
export function List<T>({ items, show, keyOf }: ListProps<T>) {
return <ul>{items.map((i) => <li key={keyOf(i)}>{show(i)}</li>)}</ul>;
}
// T is determined automatically at the call site:
<List
items={users} // T = User
keyOf={(u) => u.id} // u is User
show={(u) => u.name} // u is User
/> Does it stick?
4 questions on this lesson. Wrong answers show up in your stats.
Now write it yourself
Finish Badge: the text arrives as children, tone colours it. On top of that it should pass ordinary <span> attributes such as title through.
- □
<Badge>New</Badge>shows the textNew - □
tone="danger"setsdata-tone="danger", the default isneutral - □A
titlepassed in ends up on the<span>
import type { ComponentProps } from "react"; // TODO: type the props (children + tone + ordinary span attributes) export function Badge(props: any) { return <span>???</span>; } export default function App() { return ( <div style={{ fontFamily: "system-ui", padding: 16, display: "flex", gap: 8 }}> <Badge>New</Badge> <Badge tone="danger" title="Careful">Error</Badge> </div> ); }