React · Auth & Quality

TypeScript with React

TypeScript catches the most common React bugs — a missing prop, a misspelled field from the API, a wrong event type — while you type. It is now the default for professional React projects.

Advanced 6 min Lesson 34 of 42Examples target React 19 with Vite.
0/42 in React
On this page · 6 min · Advanced

Learning objectives#

  • Type component props and children
  • Type useState, useRef and event handlers
  • Write a generic component
  • Type API responses

Explanation#

Start with npm create vite@latest app -- --template react-ts. Component files use the .tsx extension.

Describe props with a type or interface: type ButtonProps = { label: string; onClick: () => void; variant?: "primary" | "ghost" }. Optional props use ?; children is React.ReactNode.

useState infers simple types; give a type when the initial value is empty: useState<User | null>(null), useState<Product[]>([]).

Events: React.ChangeEvent<HTMLInputElement>, React.FormEvent<HTMLFormElement>, React.MouseEvent<HTMLButtonElement>. Refs: useRef<HTMLInputElement>(null).

Generic components work with any item type: function List<T>({ items, render }: { items: T[]; render: (item: T) => React.ReactNode }).

Type API data at the boundary: const data = (await res.json()) as Product[] — and validate untrusted data with a schema library (Zod) when it matters.

Syntax#

TSX Illustrative
type Props = { title: string; count?: number; children?: React.ReactNode };


function Panel({ title, count = 0, children }: Props) {
  const [open, setOpen] = useState<boolean>(true);
  return <section><h2>{title} ({count})</h2>{open && children}</section>;
}

Code example: Typed props, state, events and a generic list#

TSXProductPicker.tsx Runnable
import { useState, type ChangeEvent, type FormEvent, type ReactNode } from 'react';


type Product = { id: number; name: string; price: number; category: 'Stationery' | 'Bags' };


type ListProps<T> = {
  items: T[];
  getKey: (item: T) => string | number;
  render: (item: T) => ReactNode;
  empty?: string;
};


function List<T>({ items, getKey, render, empty = 'Nothing to show' }: ListProps<T>) {
  if (items.length === 0) return <p>{empty}</p>;
  return <ul>{items.map(item => <li key={getKey(item)}>{render(item)}</li>)}</ul>;
}


type PickerProps = { products: Product[]; onPick: (product: Product) => void };


function ProductPicker({ products, onPick }: PickerProps) {
  const [query, setQuery] = useState('');                      // inferred as string
  const [picked, setPicked] = useState<Product | null>(null);  // explicit: starts null


  const handleChange = (e: ChangeEvent<HTMLInputElement>) => setQuery(e.target.value);


  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const match = products.find(p => p.name.toLowerCase() === query.toLowerCase());
    if (match) { setPicked(match); onPick(match); }
  };


  const visible = products.filter(p => p.name.toLowerCase().includes(query.toLowerCase()));


  return (
    <form onSubmit={handleSubmit}>
      <input value={query} onChange={handleChange} aria-label="Product" />
      <List items={visible} getKey={p => p.id} render={p => `${p.name} — ₹${p.price}`} />
      {picked && <p>Picked: {picked.name} ({picked.category})</p>}
    </form>
  );
}


export default function App() {
  const products: Product[] = [
    { id: 1, name: 'Pen', price: 20, category: 'Stationery' },
    { id: 2, name: 'Backpack', price: 1200, category: 'Bags' },
  ];
  return <ProductPicker products={products} onPick={p => console.log('picked', p.id)} />;
}
Expected output
Renders an input and the list “Pen — ₹20”, “Backpack — ₹1200”. Typing “pen” and pressing Enter shows “Picked: Pen (Stationery)”. In the editor, writing category: "Toys", forgetting the onPick prop, or calling p.prize all show TypeScript errors before the code runs.

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. Product describes the data shape; category is restricted to two literal values.
  2. List<T> is generic: TypeScript infers T = Product from items, so render receives a typed product.
  3. useState<Product | null>(null) allows the state to hold a product later.
  4. Event handler parameters use React’s event types, so e.target.value is known to be a string.
  5. Props types make required props (products, onPick) mandatory at every call site.

Real-world use case#

When the backend renames price to unitPrice, the team updates the Product type once; the compiler then lists every component that still reads .price, so nothing is missed.

Common mistakes#

useState(null) without a type

TypeScript infers the type null, so setUser(user) is an error. Write useState<User | null>(null).

Using any for props or API data

any turns checking off exactly where bugs hide. Write the type once and reuse it.

React.FC everywhere

It is allowed, but plain functions with typed props are simpler and handle generics better.

Best practices#

  • Start with the react-ts template and strict mode
  • Type props with type aliases or interfaces
  • Type API data in one shared file
  • Use generics for reusable list/table components

Remember this#

Props: type Props = { ... }; function C({ ... }: Props).

Empty initial state needs a type: useState<User | null>(null).

Events: ChangeEvent<HTMLInputElement>, FormEvent<HTMLFormElement>.

Practice exercise#

Your task: Type a Button component with props label (string), onClick (no arguments, returns nothing), optional disabled (boolean) and variant ("primary" | "danger", default "primary").

Need a hint?
type ButtonProps = { label: string; onClick: () => void; disabled?: boolean; variant?: "primary" | "danger" }.
Show solution
TSX Runnable
type ButtonProps = {
  label: string;
  onClick: () => void;
  disabled?: boolean;
  variant?: 'primary' | 'danger';
};


function Button({ label, onClick, disabled = false, variant = 'primary' }: ButtonProps) {
  return (
    <button onClick={onClick} disabled={disabled} style={{ color: variant === 'danger' ? 'red' : 'blue' }}>
      {label}
    </button>
  );
}


export default function Demo() {
  return <Button label="Delete" variant="danger" onClick={() => console.log('deleted')} />;
}

Mini quiz#

TypeScript with React quizMultiple choice Question 1 of 3

How do you type state that starts as null and later holds a User?

Score so far: 0/0

Want more? Practise all React MCQs · React interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.