Home/TypeScript with React
TypeScript with React: Complete Guide
Typing React components correctly eliminates an entire class of prop bugs and makes your IDE autocomplete genuinely useful. This guide covers every pattern you will encounter — from simple props to generic components.
Typing Functional Components
The simplest approach is to define a props interface and pass it directly to the function parameter. Avoid React.FC — it is no longer recommended and was removed from many starter templates.
import React from "react";
// Define props as an interface
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean; // optional
variant?: "primary" | "secondary" | "ghost";
}
// Type the function parameter directly — no React.FC needed
function Button({ label, onClick, disabled = false, variant = "primary" }: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant}`}
>
{label}
</button>
);
}
// Usage — TypeScript errors if label or onClick is missing
<Button label="Save" onClick={() => console.log("saved")} />
<Button label="Cancel" onClick={handleCancel} variant="ghost" />Props: interface vs type
Both interface and type work for props. Use type when you need union types or computed props.
// interface — extendable, conventional for object shapes
interface CardProps {
title: string;
description: string;
imageUrl?: string;
}
// Extend for a special variant
interface FeatureCardProps extends CardProps {
badge: string;
badgeColor: "green" | "blue" | "red";
}
// type — required when props include union or conditional logic
type AlertProps =
| { type: "success"; message: string }
| { type: "error"; message: string; code: number }
| { type: "info"; message: string; dismissible?: boolean };
function Alert(props: AlertProps) {
if (props.type === "error") {
// TypeScript knows 'code' is available here
return <div>Error {props.code}: {props.message}</div>;
}
return <div>{props.message}</div>;
}useState with TypeScript
TypeScript infers state type from the initial value. When the initial value is null or undefined, you must provide the generic argument.
import { useState } from "react";
interface User {
id: number;
name: string;
email: string;
}
function UserProfile() {
// Inferred from initial value — no annotation needed
const [count, setCount] = useState(0); // number
const [name, setName] = useState(""); // string
const [active, setActive] = useState(false); // boolean
// Must specify type when initial value is null
const [user, setUser] = useState<User | null>(null);
// Array of objects — infer from initial value or specify explicitly
const [users, setUsers] = useState<User[]>([]);
// Object state
const [form, setForm] = useState({ email: "", password: "" });
// setForm({ email: "a@b.com", password: "secret" }); ✓
// setForm({ email: "a@b.com" }); ✗ missing password
return <div>{user?.name}</div>;
}useRef with TypeScript
useRef has two distinct uses: holding a DOM node and holding a mutable value. They are typed differently.
import { useRef, useEffect } from "react";
function SearchInput() {
// DOM ref — initialize with null, TypeScript enforces null check
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// Must check .current before accessing — it starts as null
inputRef.current?.focus();
}, []);
return <input ref={inputRef} type="search" />;
}
function Timer() {
// Mutable ref — no null, holds a timer ID
const timerId = useRef<ReturnType<typeof setInterval>>(null!);
function start() {
timerId.current = setInterval(() => console.log("tick"), 1000);
}
function stop() {
clearInterval(timerId.current);
}
return (
<div>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}Typing Event Handlers
React ships with typed event interfaces for every DOM event. The element type is the generic argument.
import React from "react";
function Form() {
// ChangeEvent<HTMLInputElement> for text inputs
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value); // string — fully typed
};
// ChangeEvent<HTMLSelectElement> for dropdowns
const handleSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
console.log(e.target.value);
};
// FormEvent<HTMLFormElement> for form submission
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// process form data
};
// MouseEvent<HTMLButtonElement> for button clicks
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.clientX, e.clientY); // mouse position
};
// KeyboardEvent<HTMLInputElement> for key presses
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") console.log("submitted");
};
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} onKeyDown={handleKeyDown} />
<select onChange={handleSelect}><option>A</option></select>
<button type="button" onClick={handleClick}>Click me</button>
</form>
);
}Typing Children
import React from "react";
// ReactNode accepts any valid JSX child
interface WrapperProps {
children: React.ReactNode;
className?: string;
}
function Card({ children, className = "" }: WrapperProps) {
return <div className={`card ${className}`}>{children}</div>;
}
// ReactElement accepts only JSX elements (not strings/numbers)
interface IconButtonProps {
icon: React.ReactElement;
label: string;
onClick: () => void;
}
function IconButton({ icon, label, onClick }: IconButtonProps) {
return (
<button onClick={onClick} aria-label={label}>
{icon}
<span>{label}</span>
</button>
);
}
// PropsWithChildren utility — adds children automatically
import { PropsWithChildren } from "react";
function Section({ children, title }: PropsWithChildren<{ title: string }>) {
return (
<section>
<h2>{title}</h2>
{children}
</section>
);
}Generic React Components
Components can be generic just like functions. This is especially useful for list, table, and select components that work with any data type.
// A type-safe list component — works with any item type
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
emptyMessage?: string;
}
function List<T>({ items, renderItem, keyExtractor, emptyMessage = "No items" }: ListProps<T>) {
if (items.length === 0) return <p>{emptyMessage}</p>;
return (
<ul>
{items.map((item, i) => (
<li key={keyExtractor(item)}>{renderItem(item, i)}</li>
))}
</ul>
);
}
// Usage — T is inferred from 'items'
<List
items={users}
keyExtractor={(u) => u.id}
renderItem={(u) => <span>{u.name}</span>}
/>Frequently Asked Questions
Should I use interface or type for React component props?
Both work. The React community conventionally uses 'interface' because it is extendable and produces clearer error messages. Use 'type' when you need unions, intersections, or computed types in your props.
How do I type useState in TypeScript?
TypeScript usually infers the state type from the initial value. When you initialize with null (common for async data), provide the type explicitly: useState<User | null>(null). The setter will also be typed correctly.
How do I type a React event handler in TypeScript?
Use React.ChangeEvent<HTMLInputElement> for input onChange, React.MouseEvent<HTMLButtonElement> for button onClick, React.FormEvent<HTMLFormElement> for form onSubmit. These give full autocomplete on the event object.
What is React.FC and should I use it?
React.FC is no longer recommended. It added implicit children to props (removed in React 18's types) and does not add real value. The modern approach: define props with an interface and type the function directly as function MyComponent(props: MyProps) {}.
How do I type useRef in TypeScript?
For DOM refs, initialize with null: useRef<HTMLInputElement>(null). For mutable values like a timer ID: useRef<number>(0). TypeScript enforces null checks for DOM refs before accessing .current.
How do I type the children prop in TypeScript React?
Use React.ReactNode as the children type. It accepts strings, numbers, JSX elements, arrays, fragments, and null/undefined. If you only want JSX elements, use React.ReactElement instead.
How do I type children in a React component with TypeScript?
Use React.ReactNode for any renderable children: interface Props { children: React.ReactNode }. For components that only accept a single React element, use React.ReactElement. Avoid React.FC — it is no longer the community recommendation since React 18.
How do I type useRef in TypeScript?
For DOM refs: const ref = useRef<HTMLDivElement>(null) — the generic is the element type, and pass null as the initial value. For mutable refs (not attached to DOM): const countRef = useRef<number>(0) — use the value type directly.
How do I type an async function in a React component?
Type the return as Promise<void> for event handlers (they should not return values). For data-fetching functions: const fetchUser = async (id: string): Promise<User> => { ... }. Avoid making useEffect async — create an inner async function and call it inside the effect.
How do I type a generic React component?
Use a generic type parameter on the function: function List<T>({ items, renderItem }: { items: T[]; renderItem: (item: T) => React.ReactNode }) { return <>{items.map(renderItem)}</> }. This makes the component reusable for any item type.
Convert your React JS to React TSX
Paste a JavaScript React component and get back properly typed TypeScript — props interfaces, hook types, and event handlers included.
Convert now →From the blog
View all →
Jun 8, 2026
JavaScript Object to JSON: Serialization Patterns and Common Pitfalls

Jun 8, 2026
TypeScript to JavaScript: When and How to Strip Types

Jun 4, 2026
JavaScript Object to JSON: Serialization Patterns and Common Pitfalls

Jun 4, 2026
TypeScript to JavaScript: When and How to Strip Types

Jun 1, 2026
JSON to TypeScript: Auto-Generate Interfaces from API Responses