Advanced TypeScriptIntermediate9 min07 / 7

TypeScript with React

Learn how TypeScript supercharges your React components by typing props, state, event handlers, and children — catching bugs before they ever hit the browser.

You already know how TypeScript catches mistakes in plain JavaScript files. Now imagine bringing that same safety net into a React component. Every prop your component accepts, every piece of state it holds, every button click it handles — TypeScript can verify all of it. The result is components that are easier to understand, impossible to misuse, and far less likely to blow up at runtime. We will work through four concrete skills: typing props with an interface, typing useState, typing event handlers, and typing children. By the end you will see why TypeScript and React are one of the most popular combinations in professional front-end development.

#Typing Props with an Interface

Think of it like

Props as a contract

Think of an interface like the printed label on a power adapter: it tells you exactly what voltage and plug shape is expected. If you try to plug in something that doesn't match, you know before you damage anything. An interface does the same job for your component — it documents the contract and enforces it at compile time.

Define the interface above the component; destructure props in the signature. A caller writing <UserCard name={42} age="thirty" /> gets two errors immediately — the ? marks isPremium as optional so callers can omit it.
interface UserCardProps {
  name: string;
  age: number;
  isPremium?: boolean; // optional — the ? means it may be omitted
}

function UserCard({ name, age, isPremium = false }: UserCardProps) {
  return (
    <div>
      <h2>{name}</h2>
      <p>Age: {age}</p>
      {isPremium && <span>Premium member</span>}
    </div>
  );
}

#Typing useState

Inferred vs. explicit type parameters in useState
import { useState } from "react";

// TypeScript infers: count is number
const [count, setCount] = useState(0);

// Explicit type parameter — starts null, but will become a string later
const [username, setUsername] = useState<string | null>(null);

setUsername("Alice"); // OK
// setUsername(42);  // Error: Argument of type 'number' is not assignable
Tip

When to be explicit

If the initial value is null, undefined, [], or {}, TypeScript infers a very narrow type. Write an explicit type parameter — useState<User | null>(null) — so TypeScript knows the full range of values the state can hold.

#Typing Event Handlers

ChangeEvent and MouseEvent typed with their element generics
import { useState } from "react";
import type { ChangeEvent, MouseEvent } from "react";

function SearchBox() {
  const [query, setQuery] = useState("");

  function handleChange(event: ChangeEvent<HTMLInputElement>) {
    setQuery(event.target.value); // TypeScript knows .value exists here
  }

  function handleClear(event: MouseEvent<HTMLButtonElement>) {
    event.preventDefault();
    setQuery("");
  }

  return (
    <div>
      <input value={query} onChange={handleChange} />
      <button onClick={handleClear}>Clear</button>
    </div>
  );
}
Common mistake

The element generic matters

ChangeEvent<HTMLInputElement> and ChangeEvent<HTMLSelectElement> are different types. Without the element generic, TypeScript gives you a very limited event object — event.target.value won't even be available. Always supply the HTML element type in angle brackets.

#Typing Children with ReactNode

ReactNode is the widest type — it covers everything React can render
import type { ReactNode } from "react";

interface CardProps {
  title: string;
  children: ReactNode; // accepts JSX, strings, numbers, null, arrays...
}

function Card({ title, children }: CardProps) {
  return (
    <section className="card">
      <h3>{title}</h3>
      <div className="card-body">{children}</div>
    </section>
  );
}

// <Card title="Welcome"><p>Any JSX can go here.</p></Card>
All four techniques — interface, useState, ChangeEvent, and ReactNode — working together
import { useState } from "react";
import type { ReactNode, ChangeEvent } from "react";

interface FormFieldProps {
  label: string;
  children: ReactNode;
}

function FormField({ label, children }: FormFieldProps) {
  return <label><span>{label}</span>{children}</label>;
}

function SignupForm() {
  const [email, setEmail] = useState("");

  function handleEmail(e: ChangeEvent<HTMLInputElement>) {
    setEmail(e.target.value);
  }

  return (
    <form>
      <FormField label="Email">
        <input type="email" value={email} onChange={handleEmail} />
      </FormField>
      <button type="submit">Sign up</button>
    </form>
  );
}
Quick check

You have a component that accepts a `count` prop (required number) and an `onReset` prop (optional function that takes no arguments and returns nothing). Which interface is correct?

Key takeaways

  • Define a props interface above your component to document and enforce what callers must pass.
  • Use `useState<YourType>(initialValue)` when the initial value doesn't reveal the full range of possible state.
  • Type event handlers with `ChangeEvent<HTMLInputElement>` or `MouseEvent<HTMLButtonElement>` — always include the HTML element generic.
  • Use `ReactNode` as the type for `children` in any wrapper component that accepts arbitrary JSX.
  • TypeScript errors in React components appear in your editor before you run the code, so bugs never reach the browser.
Practice challenges
Test yourself · earn XP
0/4
Predict the output#1

Type annotations are erased at runtime. What does this code print?

predict-output
import { useState } from "react";

const [username, setUsername] = useState<string | null>(null);
setUsername("Alice");

// pretend React re-runs and username is now "Alice"
const current: string | null = "Alice";
console.log(current?.toUpperCase() ?? "NO USER");
Fix the bug#2

This code has a bug — what's wrong?

fix-bug
import type { ChangeEvent } from "react";

function handleChange(event: ChangeEvent) {
  console.log(event.target.value);
}

// used as: <input onChange={handleChange} />
Fill in the blank#3

Complete the type for children so this wrapper component accepts any JSX, strings, numbers, or arrays — the widest renderable type from the lesson.

import type {  } from "react";

interface CardProps {
  title: string;
  children: ;
}
Reorder the lines#4

Put these lines in the correct order to define a props interface and a component that destructures those props — matching the lesson's UserCard style.

1
interface UserCardProps {
2
}
3
  return <h2>{name}</h2>;
4
  isPremium?: boolean;
5
function UserCard({ name, isPremium = false }: UserCardProps) {
6
  name: string;
Your turn
Practice exercise

Build a typed Badge component. It should accept a label (string, required), a color of either "blue", "green", or "red" (required), and an optional onClick handler (a function that returns void). Render a <span> that shows the label. Below it, write a BadgeDemo component that holds a count state (starts at 0) and renders a Badge with label Clicks: {count}, color "blue", and an onClick that increments the count.

Try it live — edit the code and hit Run to see the output:

solution.ts · editable