Skip to main content

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-ts
  • cd my-app
  • npm install
  • npm 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.

script.ts
// 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.

script.ts
// 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.

script.ts
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.

script.ts
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.

script.ts
 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.

script.ts
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.

script.ts
 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;
 }