React with TypeScript means writing components in .tsx files where props, state, refs and event handlers have types, so the compiler catches a missing prop or a wrong event handler before the code runs. The types disappear at build time: what runs in the browser is the same JavaScript you would have written without them. The live editors on this page run that JavaScript, and the typed version sits beside each one in a static block.
Here is the same Badge with types. count is optional (?), so the second badge falls back to the default 0, and children accepts both an element and a plain string.
type BadgeProps = {
label: string;
count?: number;
children: React.ReactNode;
};
function Badge({ label, count = 0, children }: BadgeProps) {
return (
<div>
<strong>{label}</strong> ({count})
<div>{children}</div>
</div>
);
}
<Badge count={3}>Hi</Badge>;
// Error: Property 'label' is missing in type '{ children: string; count: number; }'
Creating a TypeScript project
Vite has a React plus TypeScript template:
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
You get .tsx files, a tsconfig.json with strict on, and the React type definitions (@types/react, @types/react-dom) already installed. Vite removes types while it serves and builds but does not check them; the template's build script runs tsc first, and your editor checks as you type. Frameworks such as Next.js set up TypeScript the same way when you create a project with their CLI.
Typing props
Describe the props as an object type and annotate the destructured parameter. A few patterns cover most components:
interface UserCardProps {
name: string; // required
age?: number; // optional, may be undefined
role: 'admin' | 'member'; // a union of allowed strings
tags: string[];
onSelect: (id: string) => void; // a callback prop
children?: React.ReactNode; // anything React can render
}
function UserCard({ name, age, role, tags, onSelect, children }: UserCardProps) {
// ...
}
type or interface. Both work for props. interface can be extended with extends and merged by declaring it twice; type can also express unions, intersections and mapped types. Most teams pick one and use it everywhere.
children. Use React.ReactNode for anything that goes between the tags: elements, strings, numbers, arrays, null. The props page explains how children arrives in the first place.
Why React.FC is optional. const Badge: React.FC<BadgeProps> = (...) => ... types the whole function instead of the parameter. Since the React 18 types it no longer adds children for you, it cannot take a type parameter for a generic component, and it adds nothing a typed parameter does not. Older code uses it widely; plain functions with typed props are the common choice for new code.
Typing useState
For a simple initial value, inference is enough: useState(0) is a number, useState('') a string. Pass a type argument when the state can hold more than its initial value suggests: a value that starts as null, or one of several fixed strings.
Click "Load user" and the status moves from idle to loading to done. The typed state:
type User = { id: number; name: string };
type Status = 'idle' | 'loading' | 'done';
const [user, setUser] = useState<User | null>(null);
const [status, setStatus] = useState<Status>('idle');
user.name; // Error: 'user' is possibly 'null'
user?.name; // OK
setStatus('ready'); // Error: Argument of type '"ready"' is not assignable to parameter of type 'SetStateAction<Status>'
Without <User | null>, useState(null) infers the type null and setUser(data) fails to compile. The union forces you to handle the empty case, which is exactly the user ? ... : ... check in the running example.
useRef for DOM elements
A ref that points at a DOM element takes the element type and starts as null, because the element does not exist until React attaches it after the first render.
Type something, then click "Log the value": the console prints what you typed, read straight from the DOM element. The typed version:
const inputRef = useRef<HTMLInputElement>(null);
// inputRef.current is HTMLInputElement | null
inputRef.current?.focus(); // OK
inputRef.current.focus(); // Error: 'inputRef.current' is possibly 'null'
const timerId = useRef<number | null>(null); // a mutable value, not a DOM node
Use the element interface that matches the tag: HTMLInputElement, HTMLDivElement, HTMLCanvasElement, HTMLButtonElement. In the React 19 types useRef always takes an argument and returns a RefObject whose current you can assign, so the same form works for DOM refs and for values like a timer id. See useRef for the runtime side.
Event handler types
Inline handlers need no annotation: in onChange={(e) => setName(e.target.value)} TypeScript knows e from the prop it is passed to. Annotate when you move a handler into its own function.
Submit and the console logs the address; preventDefault stops the page from reloading. With types:
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setEmail(e.target.value); // e.target is HTMLInputElement, value is string
}
function handleSubmit(e: React.SubmitEvent<HTMLFormElement>) {
e.preventDefault();
}
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {}
function handleKey(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter') { /* ... */ }
}
Since @types/react 19.2.10, onSubmit is typed with React.SubmitEvent and the older React.FormEvent is marked deprecated; with an older version of the types, write React.FormEvent<HTMLFormElement> instead. The type parameter is the element the handler is attached to. If you are unsure of the type, hover the onChange prop in your editor, or type the whole handler at once: const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { ... }.
Typing context
Give createContext the value type. If a sensible default exists, pass it, and every consumer gets a non-null value:
type Theme = 'light' | 'dark';
const ThemeContext = createContext<Theme>('light');
const theme = useContext(ThemeContext); // Theme
When there is no good default (a logged-in user, a store), start with null and wrap useContext in a hook that throws if a component is used outside the provider. Every caller then gets the non-null type and a clear error message instead of a crash deep inside.
type Auth = { user: string; logout: () => void };
const AuthContext = createContext<Auth | null>(null);
export function useAuth(): Auth {
const value = useContext(AuthContext);
if (!value) throw new Error('useAuth must be used inside <AuthProvider>');
return value;
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState('Ada');
return (
<AuthContext value={{ user, logout: () => setUser('') }}>
{children}
</AuthContext>
);
}
<AuthContext value={...}> is the React 19 provider syntax; <AuthContext.Provider value={...}> is typed the same way.
Generic components
A list, table or select that works with any item type is a generic component: a type parameter connects the items prop to the renderItem callback.
The typed List:
type ListProps<T> = {
items: T[];
renderItem: (item: T) => React.ReactNode;
getKey: (item: T) => string | number;
};
function List<T>({ items, renderItem, getKey }: ListProps<T>) {
return <ul>{items.map((item) => <li key={getKey(item)}>{renderItem(item)}</li>)}</ul>;
}
<List items={books} getKey={(b) => b.isbn} renderItem={(b) => b.title} />;
// b is inferred as the book type, so b.titel would be an error
TypeScript infers T from items, so callers never write the type parameter. In a .tsx file an arrow function needs <T,> instead of <T>, because <T> alone reads as a JSX tag.
Extending native element props
A wrapper around a button or input should accept every attribute the native element does. React.ComponentProps<'button'> is that full set; add your own props on top and spread the rest.
type ButtonProps = React.ComponentProps<'button'> & {
variant?: 'primary' | 'ghost';
};
function Button({ variant = 'primary', style, ...rest }: ButtonProps) {
return (
<button
{...rest}
style={{ fontWeight: variant === 'primary' ? 600 : 400, ...style }}
/>
);
}
<Button type="submit" disabled onClick={(e) => console.log(e.currentTarget)}>
Save
</Button>;
onClick, disabled, type and aria-* are all typed without listing them. In React 19 ref is a regular prop, and ComponentProps<'button'> includes it, so <Button ref={buttonRef}> reaches the DOM node through the spread with no forwardRef. Use ComponentPropsWithoutRef<'button'> when you want to leave ref out, and ComponentProps<typeof UserCard> to read the props of one of your own components.
Common type errors
"'ref.current' is possibly 'null'". The element does not exist during the first render. Use ref.current?.focus(), or check if (ref.current) before using it.
"Type 'string' is not assignable" on a union state. const [status, setStatus] = useState('idle') infers string, which is wider than you want, and passing it to a prop typed Status fails. Write useState<Status>('idle').
Event typed as the wrong element. React.ChangeEvent<HTMLInputElement> on a select makes e.target the wrong type. The type parameter must match the element the handler is attached to.
Using any to silence an error. It switches checking off for everything that value touches. Prefer unknown and narrow it, or fix the type at its source.
Frequently Asked Questions
How do I create a React app with TypeScript?
Run npm create vite@latest my-app -- --template react-ts, then npm install and npm run dev. Components go in .tsx files and Vite strips the types as it builds; run tsc (the template's build script does) to check them.
What is the type for children in React?
React.ReactNode. It accepts anything React can render: elements, strings, numbers, arrays, null and undefined. Use React.ReactElement only when you need exactly one element.
How do I type useState with null?
Pass the type explicitly as a union: useState<User | null>(null). Without the type argument TypeScript infers null as the only allowed value.
What is the type of an onChange event in React?
React.ChangeEvent<HTMLInputElement> for an input (HTMLTextAreaElement or HTMLSelectElement for those elements). e.target.value is then typed as string.
Should I use React.FC?
It is optional. Typing the props parameter directly (function Card({ title }: CardProps)) does the same job, reads more simply and works with generics. React.FC no longer adds children implicitly, so it gives you little extra.
Should I use type or interface for props?
Either works. interface can be extended and merged; type can express unions and mapped types. Pick one convention for the codebase and keep it.