← back to section

JavaScript catches errors at runtime — when the program is already running for the user. TypeScript catches errors earlier: while you are writing code in the editor. This changes how development feels: instead of "run and check," you get an instant hint right in the code.

In React, TypeScript is especially useful in a few places: component props, events, state, and references to DOM elements. Let's go through each one.

Why TypeScript if you already have JavaScript

In plain JavaScript, a component accepts anything. Say there's a button:

function Button({ variant, onClick, children }) {
  return <button className={variant} onClick={onClick}>{children}</button>;
}

Nothing stops you from calling it like this: <Button variant="primry" /> — a typo in the variant name, onClick not passed. JavaScript will stay silent. The user will see a broken button.

TypeScript adds a description of what the component expects. Pass something wrong — the editor underlines the error before the code even runs. This is static typing: checking before execution, not during.

Typing props

Props are the data a parent passes to a component. They are described using type or interface. In practice, type is more commonly used — it is slightly more flexible.

type ButtonProps = {
  variant: "primary" | "secondary";
  disabled?: boolean;
  onClick: () => void;
  children: React.ReactNode;
};

function Button({ variant, disabled = false, onClick, children }: ButtonProps) {
  return (
    <button className={variant} disabled={disabled} onClick={onClick}>
      {children}
    </button>
  );
}

A few details:

  • "primary" | "secondary" — instead of a plain string. Now variant="primry" won't compile, and the editor will suggest the valid options.
  • disabled? — the question mark means the prop is optional.
  • React.ReactNode — the type for children: accepts text, elements, arrays, and null.

Reusable components and generics

Imagine a list of products. Then a list of orders. Then a list of users. Writing a separate list component each time is inconvenient — it's easier to have one that works with any data.

For this, TypeScript offers generics. The letter T in angle brackets means "whatever type the caller will provide":

type ListProps<T> = {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyOf: (item: T) => string;
};

function List<T>({ items, renderItem, keyOf }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={keyOf(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

When we use List<Product>, TypeScript knows: renderItem receives a Product, not just anything. If you try to access a non-existent field — an error in the editor, not in the browser.

States with multiple variants

Here's a common situation: a component loads data from the server. While loading — a spinner; if there's an error — a message; if ready — the data. How do you model this?

The first impulse is a set of optional flags:

type State = {
  isLoading?: boolean;
  error?: string;
  data?: Product;
};

The problem: this type allows nonsensical combinations — for example, isLoading: true and data: {...} at the same time. TypeScript won't object, and the component will behave unpredictably.

The solution is a discriminated union: a union of types with a shared tag field (status) that lets TypeScript know exactly which state you're in.

type RemoteData<T> =
  | { status: "loading" }
  | { status: "error"; error: string }
  | { status: "success"; data: T };

function ProductView({ state }: { state: RemoteData<Product> }) {
  switch (state.status) {
    case "loading": return <Spinner />;
    case "error":   return <ErrorBox message={state.error} />;
    case "success": return <ProductCard product={state.data} />;
  }
}

In each branch of the switch, TypeScript knows exactly which fields are available: in the "success" branch there is state.data, in "error"state.error. Trying to access a field from another branch is a compile error.

Typing events and ref

React events have specific types. The type depends on which element the event occurs on and which handler it is.

function SearchInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    inputRef.current?.focus();
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={inputRef} onChange={handleChange} />
    </form>
  );
}

Useful event types:

  • React.ChangeEvent<HTMLInputElement> — input field change
  • React.FormEvent<HTMLFormElement> — form submission
  • React.MouseEvent<HTMLButtonElement> — button click

useRef<HTMLInputElement>(null) — a typed reference to a DOM element. The editor will suggest all available methods and properties of the element.

Derived types: don't repeat yourself

TypeScript lets you build new types from existing ones without copying them. Three commonly used techniques:

type Product = {
  id: string;
  name: string;
  price: number;
  createdAt: string;
};

// Creation form: everything except server-generated fields
type CreateProductInput = Omit<Product, "id" | "createdAt">;

// Edit form: all fields are optional
type UpdateProductInput = Partial<Product>;

// Name and price only, for a preview card
type ProductPreview = Pick<Product, "name" | "price">;

The point: if Product changes, derived types update automatically. One source of truth instead of manual synchronization.

In short

  • TypeScript checks types before running — errors are visible in the editor, not to the user.
  • Props are described with type or interface; narrow string literal unions instead of string catch typos.
  • Reusable components (list, table, select) are made generic with <T> so types are not lost.
  • Mutually exclusive states are described with a discriminated union with a tag field — this eliminates impossible combinations.
  • The event type depends on the element: React.ChangeEvent<HTMLInputElement>, React.MouseEvent<HTMLButtonElement>, etc.
  • useRef<HTMLElement>(null) — a typed reference to a DOM node.
  • Omit, Partial, Pick — build new types from existing ones without duplication.
  • Hooks: custom hooks and useEffect pitfalls — how to type custom hooks.
  • Forms and validation — types for form fields and validation schemas.
  • Project structure and components — how to organize typed components in a project.