TypeScript with React
This chapter shows how to start a React project with TypeScript and how to type the things you meet every day: props, events, state, refs, children, data fetching and context.
Why Use TypeScript with React?
TypeScript improves React with:
- Type safety for props, state and context
- Better IDE autocompletion and refactoring
- Errors caught early, while you write the code
Note: This chapter assumes you know the basics of React. If you are new to React, work through a React tutorial first.
Getting Started
Create a new React + TypeScript app with Vite by running these commands:
npm create vite@latest my-app -- --template react-tscd my-appnpm installnpm run dev
Your tsconfig.json should have these recommended compilerOptions:
"target": "ES2020""lib": ["ES2020", "DOM", "DOM.Iterable"]"module": "ESNext""moduleResolution": "Node""jsx": "react-jsx""strict": true"skipLibCheck": true"noEmit": true"resolveJsonModule": true"allowSyntheticDefaultImports": true"esModuleInterop": true"forceConsistentCasingInFileNames": true
with "include": ["src"] next to it.
Note: Keep strict enabled for the best type safety. These options work well with Vite and Create React App.
Component Typing
Describe the props with a type, then use that type in a function component:
Typed props in a function component
GreetingProps has a required name and an optional age; the component only renders the age line when it is given.
// Greeting.tsx
type GreetingProps = {
name: string;
age?: number;
};
export function Greeting({ name, age }: GreetingProps) {
return (
<div>
<h2>Hello, {name}!</h2>
{age !== undefined && <p>You are {age} years old</p>}
</div>
);
}
Common Patterns
These are the typing patterns you will use most often in a React app.
Type-safe events. Give event handlers for inputs and buttons the matching React event type.
Typing state with useState. Use explicit type arguments for numbers, unions and nullable values.
useRef with DOM elements. Type a ref with the DOM element it will hold so you can read its properties safely.
Children typing. Accept children with the React.ReactNode type.
Fetch helpers with generics. Use a generic function to type API responses.
Minimal context and custom hook. Provide a small, typed context plus a helper hook that guarantees it is available.
Vite TypeScript types: Add Vite's ambient types so nothing is missing. Create src/vite-env.d.ts containing /// <reference types="vite/client" />, or add "types": ["vite/client"] to compilerOptions in tsconfig.json.
About React.FC: Prefer plainly typed function components. React.FC is optional; it adds children implicitly, but it is not required.
Optional baseUrl and paths: These can make imports shorter if your bundler supports them. Set "baseUrl": "." and "paths": { "@/*": ["src/*"] } in compilerOptions. Configure them only if your tooling (for example Vite or tsconfig-paths) is set up for path aliases.
Type-safe event handlers
React.ChangeEvent<HTMLInputElement> and React.MouseEvent<HTMLButtonElement> give you typed access to the event and its target.
// Input change
function NameInput() {
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e.target.value);
}
return <input onChange={handleChange} />;
}
// Button click
function SaveButton() {
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
e.preventDefault();
}
return <button onClick={handleClick}>Save</button>;
}
Typing state with useState
A number, a union of string literals, and a nullable object, each with an explicit type argument.
const [count, setCount] = React.useState<number>(0);
const [status, setStatus] = React.useState<'idle' | 'loading' | 'error'>('idle');
type User = { id: string; name: string };
const [user, setUser] = React.useState<User | null>(null);
useRef with a DOM element
The ref is typed as HTMLInputElement, so inputRef.current?.select() is checked by the compiler.
function FocusInput() {
const inputRef = React.useRef<HTMLInputElement>(null);
return <input ref={inputRef} onFocus={() => inputRef.current?.select()} />;
}
Children typing
children is optional and typed as React.ReactNode, so anything React can render is accepted.
type CardProps = { title: string; children?: React.ReactNode };
function Card({ title, children }: CardProps) {
return (
<div>
<h2>{title}</h2>
{children}
</div>
);
}
Generic fetch helper
fetchJson<T> returns a Promise<T>, so the caller decides the shape of the response.
async function fetchJson<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error('Network error');
return res.json() as Promise<T>;
}
// Usage inside an async function/component effect
async function loadPosts() {
type Post = { id: number; title: string };
const posts = await fetchJson<Post[]>("/api/posts");
console.log(posts);
}
Typed context and custom hook
The context value is either the theme object or null; useTheme throws if it is used outside the provider.
type Theme = 'light' | 'dark';
const ThemeContext = React.createContext<{ theme: Theme; toggle(): void } | null>(null);
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = React.useState<Theme>('light');
const value = { theme, toggle: () => setTheme(t => (t === 'light' ? 'dark' : 'light')) };
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
function useTheme() {
const ctx = React.useContext(ThemeContext);
if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
return ctx;
}