[REACT](https://www.kirupa.com/react/index.htm)
[BOOK](https://www.amazon.com/exec/obidos/ASIN/0134546318/kirupacom)
# Using React with TypeScript
by [ kirupa](https://www.kirupa.com/me/index.htm) | filed under [Learning React](https://www.kirupa.com/react/index.htm)
All of the React code you have written so far has been JavaScript. JavaScript is wonderfully relaxed about what you do with it. You can pass a number where a string was expected, forget a property, or misspell the name of a function, and it will let you do all of that. It only complains when your app is running and somebody clicks the wrong thing.
[TypeScript](https://www.typescriptlang.org) is JavaScript with a safety net. You describe the shape of your data (what props a component takes, what your state holds), and TypeScript checks your code against those descriptions *before* it runs. If you pass the wrong thing, your editor underlines it right away. Almost every React job posting and open source project you will run into uses TypeScript, so it is worth getting comfortable with.
In this tutorial, we'll set up a React project with TypeScript and then walk through how to type each of the things you have been doing: props, children, state, events, refs, reducers, and context. Every example you'll see here was type checked in a real project, and the error messages are the real ones.
## Creating a React and TypeScript Project
The good news is that you don't need to install or configure anything special. The Vite setup you saw in [setting up your React environment](https://www.kirupa.com/react/setting_up_react_environment.htm) has a TypeScript template. Instead of choosing the `react` template, you choose `react-ts`:
```bash
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
```
If you run `npm create vite@latest` without the template flag, it asks you a series of questions, and you can pick React and TypeScript from the list instead. The result is the same.
Open the new project and you'll see something familiar, with a few differences:
- Your files end in `.tsx` instead of `.jsx`. A `.tsx` file is a TypeScript file that is allowed to contain JSX. (If a file has no JSX, `.ts` works too.)
- Instead of a single config file, there are a few `tsconfig` files: `tsconfig.json`, `tsconfig.app.json` (for your React code), and `tsconfig.node.json` (for the Vite config). You rarely need to touch them.
- Your `package.json` has `typescript` and the `@types/react` and `@types/react-dom` packages. Those `@types` packages are what tell TypeScript what React's functions and components look like.
There's a detail here that surprises people. Look at the scripts in `package.json`:
```json
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "oxlint",
"preview": "vite preview"
}
```
Notice that `npm run dev` just runs Vite. **Vite does not type check your code.** It strips the types out and moves on, which is part of why it is so fast. Your editor is what underlines errors as you type. The actual full check happens when you run `npm run build`, because it runs `tsc -b` (the TypeScript compiler) before Vite builds anything. If you want to check your types without building, you can run `npx tsc -b` on its own at any time.
The template also turns on strict checking, which is what you want. A few of its other settings affect how you write code, so keep these two in mind:
- `verbatimModuleSyntax` means that when you import something that is only a type, you should write `import type { ReactNode } from "react"` (or `import { type ReactNode }`), so it is clear that it disappears at build time.
- `erasableSyntaxOnly` means you can't use TypeScript features that generate real code, like `enum`. A union of strings does the same job and is what we will use.
## Typing Props
Props are where TypeScript pays for itself the fastest. You describe what a component accepts with an `interface` (a `type` works too), and then you tell the function that its props look like that:
```tsx
interface GreetingProps {
name: string;
excited?: boolean;
}
function Greeting({ name, excited = false }: GreetingProps) {
return (
Hello, {name}
{excited ? "!" : "."}
);
}
export default Greeting;
```
Here is what is going on. `name` is required and has to be a string. The `?` after `excited` makes it optional, and we give it a default of `false` in the destructuring, the same way you would in plain JavaScript. Using the component is exactly what you are used to:
```tsx
```
Now the fun part. If I forget the required prop:
```tsx
```
TypeScript stops me with this:
```
Property 'name' is missing in type '{}' but required in type 'GreetingProps'.
```
You would normally see that as a red squiggle in your editor long before you ever run `tsc`. If you want to read more on how props work in the first place, check out [creating complex components](https://www.kirupa.com/react/creating_complex_components.htm).
You will often want a prop to be one of a small set of values, such as a color. A **union of string literals** is perfect for that:
```tsx
interface BadgeProps {
label: string;
color: "blue" | "pink" | "green";
}
function Badge({ label, color }: BadgeProps) {
return {label};
}
export default Badge;
```
Ask for a color that isn't in the list, like ``, and you get:
```
Type '"red"' is not assignable to type '"blue" | "pink" | "green"'.
```
Your editor will also autocomplete the three allowed values as you type, which is a nice bonus. A small typo in a string like this is exactly the kind of thing that wastes ten minutes in plain JavaScript.
One thing you may have seen in older code is `React.FC`, as in `const Greeting: React.FC = ...`. You don't need it. Typing the props parameter directly, like we did above, is simpler and is what most people do now.
## Typing Children
When a component wraps other content, its `children` need a type too. The type for "anything React can render" (JSX elements, strings, numbers, arrays of them, and so on) is `ReactNode`:
```tsx
import type { ReactNode } from "react";
interface CardProps {
title: string;
children: ReactNode;
}
function Card({ title, children }: CardProps) {
return (
{title}
{children}
);
}
export default Card;
```
Because `children` is listed in the interface without a `?`, it is required. If I write `` with nothing inside, TypeScript lets me know:
```
Property 'children' is missing in type '{ title: string; }' but required in type 'CardProps'.
```
If your component is fine with having no children, write `children?: ReactNode` instead. It is a small thing, but it is a nice example of the type documenting how the component is meant to be used. We covered the JavaScript side of `children` in [components in React](https://www.kirupa.com/react/components.htm).
## Typing State
For `useState`, you often don't need to do anything at all. TypeScript looks at the initial value and works out the type:
```tsx
const [clicks, setClicks] = useState(0);
```
Here, `clicks` is a `number`, and `setClicks` only accepts numbers. If you try `setClicks("lots")`, you get:
```
Argument of type 'string' is not assignable to parameter of type 'SetStateAction'.
```
That `SetStateAction` is just React's name for "a value, or a function that returns a value." You don't have to remember it.
The inference breaks down when the starting value doesn't tell the whole story. The most common case is state that starts as `null` and later holds an object. For that, you give `useState` the type yourself, between angle brackets:
```tsx
import { useState } from "react";
interface User {
name: string;
age: number;
}
function Profile() {
const [clicks, setClicks] = useState(0);
const [user, setUser] = useState(null);
return (
{user ? `${user.name} is ${user.age}` : "Nobody is logged in"}
);
}
export default Profile;
```
The `User | null` says that this state is either a `User` or nothing. Notice how the last paragraph checks `user ?` first, and only then reads `user.name`. That check is not optional. If you try to read `user.name` without it, TypeScript refuses:
```
'user' is possibly 'null'.
```
This is the single most useful thing TypeScript does in React apps. It forces you to deal with the "data hasn't loaded yet" case, which is a classic source of the "cannot read properties of null" crash.
Two gotchas are worth knowing about. First, an empty array is a trap:
```tsx
const [items, setItems] = useState([]);
setItems(["a"]);
```
```
Type 'string' is not assignable to type 'never'.
```
TypeScript sees `[]` and decides it is an array that can never hold anything. The fix is to say what goes in it: `useState([])`. Second, if you leave out the initial value, as in `useState()`, the type becomes `number | undefined`, so using it as a plain number gives you:
```
Type 'number | undefined' is not assignable to type 'number'.
```
Give state a real starting value whenever you can. If you need a refresher on how state works, check out [dealing with state](https://www.kirupa.com/react/dealing_with_state.htm).
## Typing Events
Event handlers are where people tend to get stuck, mostly because the event types have long names. The pattern is simple, though. React has an event type for each kind of event, and you tell it which element the event is coming from with angle brackets. Here are the three you'll use the most, all in one form:
```tsx
import { useState } from "react";
import type { ChangeEvent, MouseEvent, SubmitEvent } from "react";
function SearchBox() {
const [query, setQuery] = useState("");
const [submitted, setSubmitted] = useState("");
function handleChange(e: ChangeEvent) {
setQuery(e.target.value);
}
function handleSubmit(e: SubmitEvent) {
e.preventDefault();
setSubmitted(query);
}
function handleClear(e: MouseEvent) {
console.log(`Clear button at ${e.clientX}, ${e.clientY}`);
setQuery("");
}
return (
);
}
export default SearchBox;
```
Each type has the same two parts: `ChangeEvent` is the kind of event, and `` is the element it came from. That second part is why `e.target.value` works in `handleChange`. TypeScript knows the target is an input, and inputs have a `value`.
There's a recent change you might trip over. Older tutorials (and a lot of AI-generated code) type a form submit handler with `FormEvent`. In the current React types, `FormEvent` is marked deprecated, and `SubmitEvent` is the replacement, which is why the code above uses it. Your editor will show `FormEvent` with a strikethrough if you use it.
And a shortcut: if you write the handler inline, you don't need to type anything. In `onClick={() => setClicks(clicks + 1)}`, TypeScript already knows what the event is, because it knows what `onClick` expects. If you leave the type off a parameter of a standalone function, though, you'll see this:
```
Parameter 'v' implicitly has an 'any' type.
```
That is TypeScript telling you it can't guess here, so you need to say what it is. For more on the events themselves, see [events in React](https://www.kirupa.com/react/events_in_react.htm).
## Typing Refs
Refs (from [accessing DOM elements](https://www.kirupa.com/react/accessing_dom_elements.htm)) take the type of the element they will point at. Because the ref starts out as `null` and only gets filled in after the component renders, you pass `null` as the initial value:
```tsx
const inputRef = useRef(null);
```
In React 19's types, you must give `useRef` an argument. Calling `useRef()` with nothing gives you `Expected 1 arguments, but got 0.`, so you'll want to remember the `null`. And since `inputRef.current` is either the input or `null`, you read it with `?.` to be safe:
```tsx
import { useRef } from "react";
import type { ComponentProps } from "react";
type TextFieldProps = ComponentProps<"input"> & {
label: string;
};
function TextField({ label, id, ...rest }: TextFieldProps) {
return (
);
}
function FocusInput() {
const inputRef = useRef(null);
function focusIt() {
inputRef.current?.focus();
}
return (
);
}
export default FocusInput;
```
There are two things to see here. The first is `ComponentProps<"input">`, which is a handy built-in type that means "all of the props a regular `` accepts." Combining it with our own `label` using `&` gives us a `TextField` that supports `placeholder`, `disabled`, `onChange`, and everything else an input does, without us listing any of them. The second is `ref`. In React 19, `ref` is a regular prop that function components can receive, so it is included in `ComponentProps` and comes through in `...rest`. You no longer need `forwardRef` for this. I loaded this component in a browser, clicked the button, and the input got focus, so it is not just a type-level trick.
## Typing useReducer
[useReducer](https://www.kirupa.com/react/managing_state_with_usereducer.htm) is a place where TypeScript really shines, because the actions you dispatch are a perfect fit for a **discriminated union**. That is a fancy name for a union of types that each have a `type` property with a unique value:
```tsx
import { useReducer } from "react";
interface CounterState {
count: number;
}
type CounterAction =
| { type: "increment" }
| { type: "decrement" }
| { type: "reset" }
| { type: "add"; amount: number };
function reducer(state: CounterState, action: CounterAction): CounterState {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "add":
return { count: state.count + action.amount };
case "reset":
return { count: 0 };
}
}
function ReducerCounter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
Count: {state.count}
);
}
export default ReducerCounter;
```
I only typed the two arguments of the `reducer` function, and everything else got figured out. Inside the `case "add"` branch, TypeScript knows that `action` has an `amount`, because that is the only action with `type: "add"`. In the other branches, `action.amount` would be an error. And `dispatch` is now strict about what you give it. A typo is caught right away:
```
Type '"incremnt"' is not assignable to type '"add" | "decrement" | "increment" | "reset"'. Did you mean '"increment"'?
```
As is forgetting a piece of an action:
```
Property 'amount' is missing in type '{ type: "add"; }' but required in type '{ type: "add"; amount: number; }'.
```
This is the main reason I like reducers with TypeScript. The list of everything that can happen in your component is written down in one place, and you can't dispatch something that isn't on the list.
## Typing Context
Context (from [sharing data with context](https://www.kirupa.com/react/sharing_data_with_context.htm)) has one awkward part with TypeScript. When you call `createContext`, you have to provide a default value, but in a real app, the actual value comes from a provider much higher up. What do you put as the default? A fake value would be misleading, so the pattern I use is to make the default `null` and then check for it in a small custom hook:
```tsx
import { createContext, use, useState } from "react";
import type { ReactNode } from "react";
type Theme = "light" | "dark";
interface ThemeContextValue {
theme: Theme;
toggleTheme: () => void;
}
const ThemeContext = createContext(null);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState("light");
function toggleTheme() {
setTheme(theme === "light" ? "dark" : "light");
}
return {children};
}
export function useTheme() {
const context = use(ThemeContext);
if (context === null) {
throw new Error("useTheme must be used inside a ThemeProvider");
}
return context;
}
```
There are a few things worth pointing out in this code. The context's type is `ThemeContextValue | null`, so it is honest about the fact that there might be no provider. The provider is written as ``, which is how React 19 does it. You no longer write `.Provider`. And the `useTheme` hook uses `use` to read the context, and throws a helpful error if the provider is missing. The nice side effect is that when `useTheme` returns, TypeScript knows the value is not `null`, so the components using it don't have to check:
```tsx
import { useTheme } from "./ThemeContext";
function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return ;
}
export default ThemeToggle;
```
The components that use the context only see a clean `theme` and `toggleTheme`, and they are not aware of the `null` at all. If you haven't used `use` before, you can learn about it in [what's new in React 19](https://www.kirupa.com/react/react_19_whats_new.htm), and the hook version of this pattern is expanded on in [building custom hooks](https://www.kirupa.com/react/custom_hooks.htm).
## Generic Components
Sometimes a component needs to work with *any* kind of data while still being type safe. A list that takes an array of heroes today and an array of products tomorrow is a good example. This is what **generics** are for. A generic component has a type parameter (conventionally called `T`) that gets filled in when you use it:
```tsx
import type { ReactNode } from "react";
interface ListProps {
items: T[];
getKey: (item: T) => string | number;
renderItem: (item: T) => ReactNode;
}
function List({ items, getKey, renderItem }: ListProps) {
return (
{items.map((item) => (
{renderItem(item)}
))}
);
}
export default List;
```
The `T` is a placeholder for "whatever type of item you give me." Using it is where the payoff is, because you almost never have to write the type out:
```tsx
interface Hero {
id: number;
name: string;
}
const heroes: Hero[] = [
{ id: 1, name: "Batman" },
{ id: 2, name: "Sherlock Holmes" },
];
hero.id}
renderItem={(hero) => {hero.name}}
/>
```
TypeScript looks at `items={heroes}`, decides that `T` must be `Hero`, and then knows that `hero` in both of the other functions is a `Hero`. Type `hero.` and your editor suggests `id` and `name`. Generics can feel abstract, but you'll write far fewer of them than you will use. They show up most in reusable components like lists, tables, and dropdowns.
## Putting It All Together
To check that all of this works in one place, I put every component from this tutorial into a single `App` and ran `npm run build`. Because the build script starts with `tsc -b`, it only gets to the Vite build if the types are clean. It was, and then I loaded the built app in a browser with no errors:
```tsx
import Badge from "./Badge";
import Card from "./Card";
import FocusInput from "./FocusInput";
import Greeting from "./Greeting";
import List from "./List";
import Profile from "./Profile";
import ReducerCounter from "./ReducerCounter";
import SearchBox from "./SearchBox";
import { ThemeProvider } from "./ThemeContext";
import ThemeToggle from "./ThemeToggle";
interface Hero {
id: number;
name: string;
}
const heroes: Hero[] = [
{ id: 1, name: "Batman" },
{ id: 2, name: "Sherlock Holmes" },
];
function App() {
return (
hero.id}
renderItem={(hero) => {hero.name}}
/>
);
}
export default App;
```
## A Few Tips Before You Go
- **Let inference do the work.** Annotate props, state that starts empty, and function parameters. Leave the rest alone. If TypeScript already knows the type, adding it yourself only creates more to maintain.
- **Avoid `any`.** It turns checking off for whatever it touches. If you don't know a type yet, `unknown` is safer because it makes you check before you use the value.
- **Don't fight the error message.** TypeScript's messages are long and look intimidating, but the last line is usually the useful one. Read it slowly and it is nearly always telling you something real.
- **Don't use `!` and `as` to silence errors.** Both exist for the rare times you know more than the compiler does. The template's `document.getElementById('root')!` is a fine example. Using them to make an annoying error go away just moves the problem to runtime.
- **You can adopt it gradually.** A project can have both `.jsx` and `.tsx` files, so there's no need to convert everything on day one.
## Conclusion
TypeScript adds a layer on top of everything you already know about React, and it doesn't change how React itself works. Props get an interface, children are a `ReactNode`, state is usually inferred (except when it starts as `null` or an empty array), events are typed with the element they came from, refs start as `null`, reducers use a discriminated union for their actions, and context gets a `null` default with a hook that checks it. In return, a whole category of bugs shows up in your editor instead of in front of your users.
It will feel like extra work for the first few days. Then you will refactor a component, watch the editor point out every place that needs updating, and it will start to feel like a superpower.
**Next tutorial:** [Testing React Apps](https://www.kirupa.com/react/testing_react_apps.htm)
Onwards!