cf.completefrontend
Open lab My progress
THE REACT FIELD GUIDE

React with TypeScript

Use TypeScript in a React 19 app: set up Vite's react-ts template, type props, events, refs, context and generic components, and fix the usual errors.

By the end, you can
  • 01
    Set up and check a React projectStart a Vite react-ts project, read its tsconfig, and know which command checks your types and which only removes them.
  • 02
    Type components the React 19 wayType props, children, events, refs as props, reducers and context, wrap native elements with ComponentProps, and write generic components.
  • 03
    Fix the common errorsRead the errors React projects hit most, like never[], possibly null and Cannot find namespace JSX, and fix each one.

A cart line that TypeScript checks

In a React project that uses TypeScript, components live in .tsx files: TypeScript with JSX. You give each component's props a type, and TypeScript checks every place the component is used. Here is a small cart. Its result is live under the code, and the list under the code is what the real TypeScript compiler says about it.

A cart line with a props type (an error on purpose)Pop out in the code editor (opens in a new tab)TSX
type CartLineProps = {  name: string;  price: number;}; function CartLine({ name, price }: CartLineProps) {  return (    <li>      {name}: ${price + 2} with delivery    </li>  );} export default function App() {  return (    <ul>      <CartLine name="Tea" price={3} />      <CartLine name="Cake" price="4" />    </ul>  );}
ResultLoading…

Lines 1 to 4 describe the props: name is a string and price is a number. Line 6 writes : CartLineProps after the props, so TypeScript knows what CartLine gets. Line 9 shows the price plus $2 for delivery. Line 17 passes the number 3, in braces. Line 18 passes "4" in quotes, which is text, and TypeScript marks it, on purpose here:

  • Type 'string' is not assignable to type 'number' means "you're passing text where CartLineProps says a number goes".

Now read the result: Tea: $5 with delivery, and Cake: $42 with delivery. The page still runs. Before the code runs, the types are removed, and "4" + 2 joins two pieces of text into "42". TypeScript doesn't change the page; it tells you about the bug before a customer sees it. Press Edit & run and change line 18 to price={4}: the Cake line says $6. (The lesson's editor shows the result as you type but doesn't check types; the list under the lesson's code does, and so does your editor in a project.)

A card App.tsx on the left with two arrows. The upper arrow leads to an editor window that marks line 18, price="4", with the note: string is not a number. The lower arrow leads to a Vite card that removes the types, then to a browser window that shows Cake: $42 with delivery.App.tsxeditorline 18: price="4"string is not a numberTypeScript checks the typesViteremoves the typesbrowserCake: $42 with deliverythe page still runs
The same App.tsx goes two ways. Your editor and tsc -b check the types and report line 18. Vite only removes the types and runs what is left, so the page still shows $42.
Real-life analogyLabels and a scanner

A warehouse labels every parcel and scans the labels at the packing desk, so a wrong parcel is caught before it leaves. The van doesn't read labels: it drives whatever it's given. A parcel that skipped the scanner still gets delivered, wrong contents and all.

In real life: A label on each parcel saying what's inside
In React: A props type, like price: number
In real life: The scanner at the packing desk that checks every label
In React: TypeScript, in your editor and in tsc -b
In real life: The van that drives parcels without reading labels
In React: Vite, which removes the types and runs the code
In real life: The customer, who never sees the labels
In React: The browser, which runs plain JavaScript
The main idea

React with TypeScript means components in .tsx files whose props, state, events, refs and context have types. TypeScript checks them in your editor and with tsc -b, before anything runs. Vite removes the types and runs plain JavaScript. React 19's types change a few older habits: a ref is a prop, JSX lives inside React, and useRef needs a starting value.

This lesson shows the React side: the project, React 19's types, and the patterns React code uses. The TypeScript course goes deeper into two parts of it: Typing React components (props, children, events, optional props) and Typing state and hooks (useState, useReducer, useRef, context, and your own hooks).

TypeScript in a Vite React project

Vite has a template for React with TypeScript called react-ts. Setting up a React project used the react template; with TypeScript, only the template's name changes:

Create a React project with TypeScriptShell
npm create vite@latest shop -- --template react-ts
cd shop
npm install
npm run dev

Line 1 makes a folder called shop from the react-ts template. Lines 2 to 4 go into it, install the packages, and start the dev server, as for a JavaScript project. The new project has a few files the JavaScript one doesn't:

What react-ts adds to a projectText
shop/
├── index.html
├── package.json
├── tsconfig.json         (points at the two files below)
├── tsconfig.app.json     (settings for your code in src)
├── tsconfig.node.json    (settings for vite.config.ts)
├── vite.config.ts
└── src/
    ├── App.tsx
    ├── main.tsx
    └── index.css

Your components end in .tsx (lines 9 and 10). There are three TypeScript settings files, because two kinds of code live in the project. tsconfig.app.json (line 5) is for your app in src, which runs in the browser. tsconfig.node.json (line 6) is for vite.config.ts, which runs in Node.js. tsconfig.json (line 4) only points at the other two. Here are the settings for your app:

tsconfig.app.json, as the template writes itJSON
{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "target": "es2023",
    "lib": ["ES2023", "DOM"],
    "module": "esnext",
    "types": ["vite/client"],
    "allowArbitraryExtensions": true,
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "verbatimModuleSyntax": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "erasableSyntaxOnly": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"]
}

The lines that matter most for React code:

  • Line 5, "lib": ["ES2023", "DOM"], gives TypeScript the browser's types, like HTMLInputElement. Line 7 adds the types for Vite's extras, like import.meta.env.
  • Line 13, allowImportingTsExtensions, lets you import "./App.tsx" with its extension. Line 14, verbatimModuleSyntax, wants imports of types marked import type (or type inside the braces), so the tools can remove them safely. That's why this lesson's examples write import type { ReactNode }.
  • Line 16, noEmit: TypeScript only checks and writes no files. Vite makes the JavaScript.
  • Line 17, "jsx": "react-jsx": JSX turns into calls to React's JSX functions, which are imported for you. You don't import React to write JSX.
  • Line 22, erasableSyntaxOnly, allows only TypeScript that can simply be deleted, like types. It rejects enums, which turn into real code.

There is no strict line. The template installs TypeScript 6.0 (create-vite 9.3.1, checked in October 2026), and since 6.0 strict checks are on unless a project turns them off; older templates wrote "strict": true. tsconfig and strictness explains each strict check. The scripts in package.json show who checks the types:

The scripts in package.jsonJSON
"scripts": {
  "dev": "vite",
  "build": "tsc -b && vite build",
  "lint": "oxlint",
  "preview": "vite preview"
}

Line 2, vite, starts the dev server. It removes the types from each file and runs the rest, without checking them. Line 3 builds for release: tsc -b checks every file first, and the && means vite build only runs if there were no errors. How to read the table: each row is a command, whether it checks types, and what it does.

Which command checks your types
When you runAre types checked?What happens
npm run devNo.Vite removes the types from each file and runs the JavaScript. A type error doesn't stop the page.
Your editorYes, as you type.It runs TypeScript in the background and marks errors in the files you open.
npm run buildYes, first.tsc -b checks every file. If it finds an error, the build stops before vite build runs.
npx tsc -bYes.The same check as the build, without building. Useful before you commit.

The entry file is the JavaScript template's, with one extra character:

src/main.tsx, as the template writes itTSX
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

Line 6 ends document.getElementById('root') with !. getElementById can return null, and createRoot needs an element. The ! tells TypeScript "this one is never null", which is true because index.html has <div id="root">. (Type assertions explains !.) Line 4 imports ./App.tsx with its extension, which line 13 of the settings allows.

To add TypeScript to a Vite project that started in JavaScript, install the compiler and React's types:

Add TypeScript to an existing Vite React projectShell
npm install --save-dev typescript @types/react @types/react-dom
npx tsc -b

Line 1 installs typescript and the type packages for react and react-dom. Keep @types/react on the same major version as React: 19 for React 19. Then copy the three tsconfig files from a new react-ts project, add tsc -b && to the build script, and rename files from .jsx to .tsx one at a time. Line 2 checks them. Migrating to TypeScript shows how to move a project a file at a time.

Props and children with React 19's types

Props types work as the overview showed: an object type, written after the props. Content between a component's tags arrives as the children prop, and its type is ReactNode: anything React can show.

A panel that takes any contentPop out in the code editor (opens in a new tab)TSX
import type { ReactNode } from "react"; type PanelProps = {  title: string;  children: ReactNode;}; function Panel({ title, children }: PanelProps) {  return (    <section>      <h2>{title}</h2>      {children}    </section>  );} export default function App() {  return (    <Panel title="Your cart">      <p>2 items, $7</p>      <button>Checkout</button>    </Panel>  );}
ResultLoading…

Line 1 imports the ReactNode type, with import type. Line 5 says children can be anything React shows: elements, text, numbers, lists of them, or nothing (null). Line 8 takes title and children out of the props. Line 12 puts the children where they belong. Lines 20 and 21, a paragraph and a button, are the children. The result shows the heading, then both.

Notice what isn't there: a return type. TypeScript works out what Panel returns, and checks at each tag that React can show it. You may meet React.FC<PanelProps> in other code. It works too, but adds nothing here, and it hasn't added children by itself since version 18 of React's types. Typing React components compares ReactNode, ReactElement, and PropsWithChildren.

Older code and tutorials often write the return type as JSX.Element. React 19's types removed the global JSX, so that line is now an error (on purpose here):

A return type written the old way (an error on purpose)Pop out in the code editor (opens in a new tab)TSX
function Price({ amount }: { amount: number }): JSX.Element {  return <strong>${amount}</strong>;} export default function App() {  return <Price amount={3} />;}
ResultLoading…
  • Cannot find namespace 'JSX' means "there is no JSX anywhere this file can see". The result still shows $3, because types never change what runs.

JSX now lives inside React, so other tools' JSX types can't clash with React's. Import it, write React.JSX.Element, or leave the return type off:

JSX, imported from reactPop out in the code editor (opens in a new tab)TSX
import type { JSX } from "react"; function Price({ amount }: { amount: number }): JSX.Element {  return <strong>${amount}</strong>;} export default function App() {  return <Price amount={3} />;}
ResultLoading…

Line 1 imports the type JSX from react, and line 3 uses it. No errors. The same move fixes custom element types: older code added them to the global JSX, and React 19 code adds them inside declare module "react" (Globals and module augmentation shows how).

Events, refs, and ref as a prop

An inline handler, like onChange={(event) => …} on an <input>, gets its event type from the element. A handler written as its own function needs the type written out. Refs need the type of the element they will hold. And in React 19, a component receives a ref like any other prop. This search box does all three:

A search box a parent can focusPop out in the code editor (opens in a new tab)TSX
import { useRef, useState, type ChangeEvent, type Ref } from "react"; type SearchInputProps = {  value: string;  onChange: (event: ChangeEvent<HTMLInputElement>) => void;  ref?: Ref<HTMLInputElement>;}; function SearchInput({ value, onChange, ref }: SearchInputProps) {  return <input ref={ref} value={value} onChange={onChange} placeholder="Search products" />;} export default function App() {  const [query, setQuery] = useState("");  const inputRef = useRef<HTMLInputElement>(null);  //    ^?   function handleChange(event: ChangeEvent<HTMLInputElement>) {    setQuery(event.target.value);  }   return (    <>      <SearchInput ref={inputRef} value={query} onChange={handleChange} />      <button onClick={() => inputRef.current?.focus()}>Focus search</button>      <p>Searching for: {query || "nothing yet"}</p>    </>  );}
ResultLoading…

Line by line:

  • Lines 3 to 7 are SearchInput's props. Line 5 types onChange as a function that takes a ChangeEvent<HTMLInputElement>: a change event from an input. Line 6 is the ref: Ref<HTMLInputElement>, optional, because a parent may not pass one.
  • Line 9 takes ref out of the props like any other prop, and line 10 puts it on the <input>.
  • Line 15 makes the ref, with null as its start. The query on line 16 shows its type, RefObject<HTMLInputElement | null>: it holds the input, or null before React attaches it.
  • Lines 18 to 20 are a named handler, so line 18 writes the event's type. event.target is then an HTMLInputElement, with a value string.
  • Line 24 passes the ref down. Line 25 focuses the input on click; ?. skips the call while current is null.

Type in the box: the sentence follows. Click elsewhere, then press Focus search: the cursor jumps into the box, because inputRef.current is the real input.

A function card App passes ref, an ordinary prop, along an arrow to a function card SearchInput, which passes it along another arrow to an input element in a page window. Under the page: inputRef.current is this input. Under SearchInput, a crossed-out card forwardRef: not needed in React 19.Appref={inputRef}a propSearchInput{ ref }page<input>inputRef.currentis this inputforwardRef(…)not needed in React 19
App passes ref={inputRef} like any prop. SearchInput takes ref and puts it on the <input>, so inputRef.current is that input. Older code needed forwardRef for this.

Before React 19, React kept ref out of a component's props. To pass one through, you wrapped the component in forwardRef. You'll still find it in older code; it works in React 19, and React plans to deprecate it:

The same input with forwardRef, as older code writes itTSX
import { forwardRef, type ChangeEvent } from "react";

type SearchInputProps = {
  value: string;
  onChange: (event: ChangeEvent<HTMLInputElement>) => void;
};

const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
  function SearchInput({ value, onChange }, ref) {
    return <input ref={ref} value={value} onChange={onChange} placeholder="Search products" />;
  },
);

Line 8 wraps the component, with the element type first and the props second; note the order is the reverse of the call's arguments. The ref arrives as a separate second argument on line 9. With React 19 you delete the wrapper and add ref to the props, as searchBox does.

Two more changes in React 19's ref types:

  • useRef needs a starting value: useRef<number>() is an error, so write useRef<number | undefined>(undefined). Every ref's current can be changed by your code; older types made some read-only.
  • A ref callback, ref={(node) => …}, may return a cleanup function, so it must return nothing else. Manipulating the DOM with refs covers ref callbacks, and the errors playground below shows the mistake.

Typing events lists the DOM's event types, and Typing state and hooks covers useRef for values as well as elements.

Wrapping a native element with ComponentProps

Apps are full of components that wrap one element: a Button around <button>, a TextField around <input>. Your wrapper should accept everything the element accepts, like onClick, disabled, type and aria-label, without listing them all. ComponentProps<"button"> is that list: the type of a <button>'s props.

A Button that takes every button prop (an error on purpose)Pop out in the code editor (opens in a new tab)TSX
import { useState, type ComponentProps } from "react"; type ButtonProps = ComponentProps<"button"> & {  variant?: "primary" | "ghost";}; function Button({ variant = "primary", className = "", ...rest }: ButtonProps) {  return <button className={`btn ${variant} ${className}`} {...rest} />;} export default function App() {  const [added, setAdded] = useState(0);   return (    <>      <Button onClick={() => setAdded(added + 1)}>Add to cart</Button>      <Button variant="ghost" type="button" disabled>        Save for later      </Button>      <Button variant="danger">Remove</Button>      <p>Added {added} times</p>    </>  );}
ResultLoading…
  • Lines 3 to 5 make ButtonProps: every <button> prop, joined with & to one of your own, variant (Intersection types explains &).
  • Line 7 takes out variant and className, with defaults. ...rest collects every other prop (Spread and rest).
  • Line 8 builds the class name and spreads rest onto the real <button>, so onClick, type, disabled and the children all reach it.
  • Lines 16 and 17 use onClick, type and disabled, which ButtonProps never names. TypeScript accepts them because ComponentProps<"button"> has them.
  • Line 20 passes variant="danger", and TypeScript marks it, on purpose here: Type '"danger"' is not assignable to type '"primary" | "ghost" | undefined' means only those two words, or nothing, may go there.

Press Add to cart: the count goes up, because onClick travelled through rest. The styles are in the styles.css tab.

A crate named button props holds five cards: onClick, disabled, type, children, ref. Next to it, a card variant. Both lead into a function card Button. Button passes variant to className, and the rest, with a spread, to a button element in a page window.onClickdisabledtypechildrenrefbutton propsComponentProps<"button">your own propvariantButton{...rest}page<button>className="btn ghost"
ButtonProps is every prop a <button> takes, plus your variant. Button turns variant into a class name and spreads the rest onto the real <button>, including ref.

ComponentProps<"button"> includes ref. In React 19 a ref is an ordinary prop of your component, so the spread on line 8 passes it to the <button> too, with no extra code. When a wrapper shouldn't accept a ref, use ComponentPropsWithoutRef:

With and without refPop out in the code editor (opens in a new tab)TSX
import type { ComponentProps, ComponentPropsWithoutRef } from "react"; type WithRef = ComponentProps<"button">["ref"];//   ^?type WithoutRef = ComponentPropsWithoutRef<"button">; const props: WithoutRef = { type: "button", disabled: true }; export default function App() {  return <button {...props}>Save for later</button>;}
ResultLoading…

The query on line 4 shows that ComponentProps<"button">["ref"] is Ref<HTMLButtonElement> | undefined. Line 5 makes the same props without ref, and line 7 uses them for a plain object that line 10 spreads onto a button. To copy another component's props instead of an element's, write ComponentProps<typeof Button>.

Reducers and context the React 19 way

useState, useReducer and context work as Typing state and hooks shows. Two of them read a little differently with React 19's types. Here is a cart in context, changed by a reducer:

A typed cart reducer in contextPop out in the code editor (opens in a new tab)TSX
import { createContext, useContext, useReducer, type ReactNode } from "react"; type Item = { name: string; price: number };type CartAction = { type: "add"; item: Item } | { type: "clear" }; function cartReducer(items: Item[], action: CartAction): Item[] {  switch (action.type) {    case "add":      return [...items, action.item];    case "clear":      return [];  }} type CartValue = { items: Item[]; dispatch: (action: CartAction) => void };const CartContext = createContext<CartValue | null>(null); function useCart() {  const cart = useContext(CartContext);  if (!cart) throw new Error("useCart must be used inside CartProvider");  return cart;} function CartProvider({ children }: { children: ReactNode }) {  const [items, dispatch] = useReducer(cartReducer, []);  return <CartContext value={{ items, dispatch }}>{children}</CartContext>;} function AddTea() {  const { dispatch } = useCart();  const tea = { name: "Tea", price: 3 };  return <button onClick={() => dispatch({ type: "add", item: tea })}>Add tea</button>;} function CartTotal() {  const { items, dispatch } = useCart();  const total = items.reduce((sum, item) => sum + item.price, 0);  return (    <p>      {items.length} items, ${total} <button onClick={() => dispatch({ type: "clear" })}>Clear</button>    </p>  );} export default function App() {  return (    <CartProvider>      <AddTea />      <CartTotal />    </CartProvider>  );}
ResultLoading…
  • Line 4 lists every action as a union, one shape per type (a discriminated union). Line 6 types the reducer's state, action, and result. Inside case "add", line 9 can read action.item, because only that action has one.
  • Line 25 calls useReducer(cartReducer, []) with no type arguments. React 19's types read the state and action types from cartReducer. Older code wrote useReducer<Reducer<State, Action>>(…), which React 19's types no longer accept.
  • Line 16 creates the context with null as its default, so its type is CartValue | null. Lines 18 to 22 are a small hook that throws if there's no provider; after line 20, TypeScript knows cart isn't null, so every component that calls useCart() gets a CartValue.
  • Line 26 renders <CartContext value={…}>: in React 19 the context itself is the provider. <CartContext.Provider> still works, and React plans to deprecate it.

Press Add tea twice: 2 items, $6. Clear sends the other action. How to read the table: each row is something you do in React code, how older code wrote it, and how it reads with React 19's types.

What React 19's types changed
What you're doingOlder codeWith React 19's types
Pass a ref to your componentWrap the component in forwardRef.Take ref as a prop, typed Ref<HTMLInputElement>.
Name the type of JSXJSX.Element, from a global namespace.React.JSX.Element, or import type { JSX } from "react".
Make a ref with no value yetuseRef<number>(), with no argument.useRef<number | undefined>(undefined) or useRef<number | null>(null). Every ref's current can be changed.
Type useReduceruseReducer<Reducer<State, Action>>(reducer, start).No type arguments: useReducer(reducer, start) reads them from the reducer.
Provide a context<CartContext.Provider value={cart}>.<CartContext value={cart}>.
Return from a ref callbackAnything: React ignored it.Nothing, or a cleanup function.

Extracting state logic into a reducer and Scaling up with reducer and context teach the patterns themselves.

Generic components

A component can be generic, like a generic function. A List<T> shows any kind of item, and TypeScript works out T from the items you pass, so the functions you give it know the item's type:

One List for products and reviewsPop out in the code editor (opens in a new tab)TSX
import type { ReactNode } from "react"; type ListProps<T> = {  items: T[];  getKey: (item: T) => string;  renderItem: (item: T) => ReactNode;}; function List<T>({ items, getKey, renderItem }: ListProps<T>) {  return (    <ul>      {items.map((item) => (        <li key={getKey(item)}>{renderItem(item)}</li>      ))}    </ul>  );} const products = [  { id: "tea", name: "Tea", price: 3 },  { id: "cake", name: "Cake", price: 4 },];const reviews = [{ email: "asha@example.com", stars: 5 }]; export default function App() {  return (    <>      <List        items={products}        getKey={(product) => product.id}        renderItem={(product) => `${product.name}: $${product.price}`}        //            ^?      />      <List items={reviews} getKey={(review) => review.email} renderItem={(review) => `${review.stars} stars`} />    </>  );}
ResultLoading…
  • Lines 3 to 7 describe the props in terms of T: an array of T, a function that gives each T a key, and one that shows a T.
  • Line 9, function List<T>, makes T a type parameter. List never looks inside an item; it only passes items to getKey and renderItem on line 13.
  • Lines 28 to 33 use List with products. TypeScript decides T is the product type, and the query on line 32 shows product has id, name and price.
  • Line 34 uses the same List with reviews: there, T is the review type.
A card items={products} on the left. An arrow leads to a function card List with T in angle brackets, labelled: T is the product type. Two arrows lead from it to two cards on the right: getKey(product) and renderItem(product), both labelled: product has id, name and price.items={products}the first prop TypeScript readsList<T>T = productgetKey(product)renderItem(product)product: id, name, price
TypeScript reads items={products} and decides T is the product type. Every other place that says T, like getKey and renderItem, gets that same type.

The type flows all the way. Read a property products don't have, and TypeScript marks it (on purpose here):

A typo in renderItem (an error on purpose)Pop out in the code editor (opens in a new tab)TSX
import type { ReactNode } from "react"; type ListProps<T> = {  items: T[];  getKey: (item: T) => string;  renderItem: (item: T) => ReactNode;}; function List<T>({ items, getKey, renderItem }: ListProps<T>) {  return (    <ul>      {items.map((item) => (        <li key={getKey(item)}>{renderItem(item)}</li>      ))}    </ul>  );} const products = [  { id: "tea", name: "Tea", price: 3 },  { id: "cake", name: "Cake", price: 4 },];const reviews = [{ email: "asha@example.com", stars: 5 }]; export default function App() {  return (    <>      <List        items={products}        getKey={(product) => product.id}        renderItem={(product) => product.title}        //            ^?      />      <List items={reviews} getKey={(review) => review.email} renderItem={(review) => `${review.stars} stars`} />    </>  );}
ResultLoading…
  • Property 'title' does not exist on type '{ id: string; name: string; price: number; }' on line 31: products have no title. In the result, the first list's two items are empty, because product.title is undefined.

With an arrow function, a .tsx file reads <T> as the start of a JSX tag. Write <T,> instead; the comma tells TypeScript it's a type parameter:

A generic arrow function in a .tsx fileTSX
const List = <T,>({ items, getKey, renderItem }: ListProps<T>) => (
  <ul>
    {items.map((item) => (
      <li key={getKey(item)}>{renderItem(item)}</li>
    ))}
  </ul>
);

Line 1 is the only difference from function List<T>. Generics in practice shows more generic patterns.

Common errors and how to fix them

These six errors come up in almost every React project with TypeScript. Pick one, read the message and why it happens, then press Fixed to see the line that fixes it. Each time, the real compiler checks the code with React's types.

Playground: read the error, then fix it
useState([]) (an error on purpose)TSX
import { useState } from "react"; export default function App() {  const [items, setItems] = useState([]);  return <button onClick={() => setItems([...items, "Tea"])}>Add tea</button>;}
TypeScript saysline 5
The message

Type 'string' is not assignable to type 'never'.

Try it yourself
Why it happens

An empty array gives TypeScript nothing to go on, so it decides items is never[]: an array that can never hold anything. The error shows where you first put something in.

The real TypeScript compiler checks the code with React's types each time you switch. Fixed changes only line 4. Reset goes back to the first error.

How to read the table: each row is a message TypeScript prints, what it means in plain words, and the fix. Keep it for when you meet one in your own project.

Six errors and their fixes
TypeScript saysWhat it meansThe fix
Type 'string' is not assignable to type 'never'.An empty array gives TypeScript nothing to go on, so it decides items is never[]: an array that can never hold anything. The error shows where you first put something in.Say what goes in where the state starts, on line 4: useState<string[]>([]).
'inputRef.current' is possibly 'null'.useRef<HTMLInputElement>(null) holds null until React attaches the input, and again after React removes it, so current is HTMLInputElement | null.Check it first: inputRef.current?.focus() does nothing while it's null.
Expected 1 arguments, but got 0.React 19's types need a starting value for useRef, the way createContext does. Older types allowed useRef().Pass the starting value, and put it in the type: useRef<number | undefined>(undefined).
Cannot find namespace 'JSX'.React 19's types removed the global JSX namespace. It lives inside React now, as React.JSX.Write React.JSX.Element, import it with import type { JSX } from "react", or leave the return type off and let TypeScript work it out.
Parameter 'event' implicitly has an 'any' type.A handler written apart from the JSX doesn't know which element it's for, so its parameter has no type. An inline onChange={(event) => …} gets its type from the input.Give the parameter the event's type: event: React.ChangeEvent<HTMLInputElement>.
Type '(node: HTMLInputElement | null) => HTMLInputElement | null' is not assignable to type 'Ref<HTMLInputElement> | undefined'.In React 19 a ref callback may return a cleanup function. The arrow without braces returns node, which isn't one, so the types reject it.Add braces, so the arrow returns nothing: (node) => { saved = node; }.

Long messages read from the top: the first line says what went wrong, and the lines under it say why. Reading type errors walks through longer ones.

A product search, typed end to end

Here are the pieces together, split into files the way a project splits them. Click the tabs to read SearchInput.tsx, Button.tsx and List.tsx.

A product search in four filesPop out in the code editor (opens in a new tab)TSX
import { useRef, useState } from "react";import Button from "./Button.tsx";import List from "./List.tsx";import SearchInput from "./SearchInput.tsx"; type Product = { id: string; name: string; price: number }; const products: Product[] = [  { id: "tea", name: "Green tea", price: 3 },  { id: "cake", name: "Carrot cake", price: 4 },  { id: "lemonade", name: "Lemonade", price: 2 },]; export default function App() {  const [query, setQuery] = useState("");  const inputRef = useRef<HTMLInputElement>(null);  const shown = products.filter((product) => product.name.toLowerCase().includes(query.toLowerCase()));   function clear() {    setQuery("");    inputRef.current?.focus();  }   return (    <>      <SearchInput ref={inputRef} value={query} onChange={(event) => setQuery(event.target.value)} />      <Button variant="ghost" onClick={clear}>        Clear      </Button>      <List items={shown} getKey={(product) => product.id} renderItem={(product) => `${product.name}: $${product.price}`} />    </>  );}
ResultLoading…
  • SearchInput.tsx takes ComponentProps<"input"> and spreads it onto an <input>. It never mentions ref, yet line 26 of App.tsx passes one and it arrives: in React 19, ref is one of the props the spread passes on.
  • On line 26 the inline onChange gets its event type from SearchInput's props, so event.target.value is a string with no type written.
  • Line 27 uses Button from Button.tsx, the wrapper from earlier, with variant="ghost".
  • Line 30 uses the generic List from List.tsx. T is Product (line 6), so product.price is a number.

Type ca in the search box: only Carrot cake stays. Press Clear: the box empties and gets the focus, through the ref. Type xyz: No products match.

Error or fine?
  • const [items, setItems] = useState<string[]>([]);
  • const timer = useRef<number>();
  • <Button variant="ghost" disabled>Save</Button>;
  • function Badge(): JSX.Element { return <strong>New</strong>; }
  • <input ref={(node) => { saved = node; }} />;
  • <input ref={(node) => (saved = node)} />;
  • const [cart, dispatch] = useReducer(cartReducer, []);
  • <CartContext value={null}>{null}</CartContext>;
Try it yourself
0 of 8 correct

Each line sits inside a React 19 project with a Button like the one above, a cartReducer, a CartContext, and a saved variable. Sort it: does TypeScript mark it, or accept it?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "The dev server stops me if my types are wrong." No. npm run dev removes the types without checking them. Your editor and tsc -b, which npm run build runs first, do the checking.
  • "Types check my props while the app runs." No. Types are gone before the code runs. Data from outside, like a server's answer, still needs checking with code.
  • "I need forwardRef to pass a ref to my component." No, not in React 19. Add ref to the props and put it on the element.
  • "Every component needs React.FC or a JSX.Element return type." No. A plain function with typed props is enough, and TypeScript works out the return type. JSX.Element isn't even global any more.
  • "useRef<HTMLInputElement>(null) gives a ref I can't change." No. In React 19's types, every RefObject's current can be changed.
  • "useReducer needs type arguments." No. Type the reducer's parameters, and useReducer reads the types from it.

Practice exercises

Each exercise shows its result under the code, and the list of TypeScript errors under the starter. Press Edit & run to change the code: the result updates as you type. Check your types with the solution's list, or in a project of your own.

Exercise 1 · Warm-upFind the line TypeScript marks

This code has no error list, on purpose. Which line does TypeScript mark? Read the result first: what does the Cake line say, and is it right?

type BadgeProps = {  name: string;  inStock: boolean;}; function StockBadge({ name, inStock }: BadgeProps) {  return <p>{name}: {inStock ? "in stock" : "sold out"}</p>;} export default function App() {  return (    <>      <StockBadge name="Tea" inStock={true} />      <StockBadge name="Cake" inStock="no" />    </>  );}
ResultLoading…

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upGive the props a type

    TypeScript marks line 1 twice. Give ProductBadge a props type. Before your fix, what type does TypeScript give price?

    function ProductBadge({ name, price }) {  return (    <span>      {name}: ${price}    </span>  );} export default function App() {  return <ProductBadge name="Tea" price={3} />;}
    ResultLoading…

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeTake a ref as a prop

      Click Focus search in the result: nothing happens. Make SearchInput take the ref the React 19 way. Before your fix, what is inputRef.current after you click?

      import { useRef } from "react"; type SearchInputProps = {  placeholder: string;}; function SearchInput({ placeholder }: SearchInputProps) {  return <input placeholder={placeholder} />;} export default function App() {  const inputRef = useRef<HTMLInputElement>(null);  return (    <>      <SearchInput ref={inputRef} placeholder="Search products" />      <button onClick={() => inputRef.current?.focus()}>Focus search</button>    </>  );}
      ResultLoading…

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFix the list that starts empty

        The result works, but TypeScript marks line 8. Fix it where the state starts. Before your fix, what type does TypeScript give items?

        import { useState } from "react"; export default function App() {  const [items, setItems] = useState([]);   return (    <>      <button onClick={() => setItems([...items, "Tea"])}>Add tea</button>      <p>        {items.length} items: {items.join(", ")}      </p>    </>  );}
        ResultLoading…

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeWrap an input with ComponentProps

          Line 14 passes type, placeholder and required, which TextField doesn't accept. Make it accept every <input> prop and pass them on. After your fix, what type does the real input get?

          type TextFieldProps = {  label: string;}; function TextField({ label }: TextFieldProps) {  return (    <label>      {label} <input />    </label>  );} export default function App() {  return <TextField label="Email" type="email" placeholder="asha@example.com" required />;}
          ResultLoading…

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeMake the picker generic

            Line 27 has two errors: getLabel can't read product.name, and setPicked doesn't fit onPick. Make Picker generic. With your fix, what does the page say after you click Cake?

            import { useState } from "react"; type PickerProps = {  options: unknown[];  getLabel: (option: unknown) => string;  onPick: (option: unknown) => void;}; function Picker({ options, getLabel, onPick }: PickerProps) {  return options.map((option) => (    <button key={getLabel(option)} onClick={() => onPick(option)}>      {getLabel(option)}    </button>  ));} type Product = { name: string; price: number };const products: Product[] = [  { name: "Tea", price: 3 },  { name: "Cake", price: 4 },]; export default function App() {  const [picked, setPicked] = useState<Product | null>(null);  return (    <>      <Picker options={products} getLabel={(product) => product.name} onPick={setPicked} />      <p>{picked ? `You picked ${picked.name}: $${picked.price}` : "Pick a product"}</p>    </>  );}
            ResultLoading…

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For code questions, find where each type comes from: a type you wrote, a starting value, or a prop TypeScript read first. Press Edit & run on a question's code to run it.

              The React with TypeScript quiz · 7 questionsScore: first tries count
              1. Question 1 of 7The overview's cart still passes price="4" on line 18, and you run npm run dev. What happens?

                Choose an answer to see the explanation.

              2. Question 2 of 7What type does inputRef have in this component?

                Read the code, then predictPop out in the code editor (opens in a new tab)TSX
                import { useRef } from "react";
                
                export function SearchBox() {
                  const inputRef = useRef<HTMLInputElement>(null);
                  return <input ref={inputRef} />;
                }

                Choose an answer to see the explanation.

              3. Question 3 of 7In React 19, how does a parent's ref reach the <input> inside your SearchInput component?

                Choose an answer to see the explanation.

              4. Question 4 of 7Which Button in this toolbar does TypeScript mark?

                Read the code, then predictPop out in the code editor (opens in a new tab)TSX
                import type { ComponentProps } from "react";
                
                type ButtonProps = ComponentProps<"button"> & { variant?: "primary" | "ghost" };
                
                function Button({ variant = "primary", ...rest }: ButtonProps) {
                  return <button className={variant} {...rest} />;
                }
                
                export function Toolbar() {
                  return (
                    <>
                      <Button variant="ghost" disabled>Save</Button>
                      <Button type="submit">Pay</Button>
                      <Button variant="outline">Share</Button>
                    </>
                  );
                }

                Choose an answer to see the explanation.

              5. Question 5 of 7What type does TypeScript work out for review in renderItem?

                Read the code, then predictPop out in the code editor (opens in a new tab)TSX
                type ListProps<T> = { items: T[]; renderItem: (item: T) => string };
                
                function List<T>({ items, renderItem }: ListProps<T>) {
                  return <ul>{items.map((item) => <li key={renderItem(item)}>{renderItem(item)}</li>)}</ul>;
                }
                
                export function Reviews() {
                  return <List items={[{ stars: 5, by: "Asha" }]} renderItem={(review) => review.by} />;
                }

                Choose an answer to see the explanation.

              6. Question 6 of 7Your React 19 project reports Cannot find namespace 'JSX' on function Badge(): JSX.Element. What's the fix?

                Choose an answer to see the explanation.

              7. Question 7 of 7Why does useState([]) cause an error as soon as you add an item?

                Choose an answer to see the explanation.

              Key takeaways

              • Start with npm create vite@latest -- --template react-ts. Vite removes types without checking them; your editor and tsc -b (run first by npm run build) check them.
              • Type props with an object type after the props, children as ReactNode, and let TypeScript work out return types. JSX lives in React now: React.JSX.Element.
              • In React 19 a ref is a prop: ref?: Ref<HTMLInputElement>, no forwardRef. useRef needs a starting value, and useRef<T>(null) holds T | null.
              • Wrap native elements with ComponentProps<"button"> and spread the rest; it includes ref. Use ComponentPropsWithoutRef when it shouldn't.
              • Type the reducer, not useReducer. Create context with null, remove it with a hook that throws, and provide it with <Context value>.
              • Generic components, function List<T>, carry the item type from one prop to the others.

              Remember the one-liner.
              Type what goes in, let TypeScript work out the rest, and remember that the checker, not the dev server, catches mistakes.

              Coming next: Rendering strategies. It starts the React on the server stage: where your components render (in the browser, on a server, or at build time) and what each choice costs.

              CompleteFrontend Clear concepts. Working examples.