DevPath · Learn to code ESPTEN

Professional React: TypeScript, SSR, and accessibility

React + TypeScript: typing your components

Why TypeScript in React

In professional projects, React is almost always written with TypeScript. The benefit is huge: the editor warns you about errors before running (a misspelled prop, a value that can be undefined, a handler with the wrong type), and the code documents itself. For React, the most important things are typing the props, the state, and the events.

Note: TypeScript compiles to JavaScript; in the browser it always ends up running JS. Here we'll see the theory with .tsx examples; the practical exercises in this module are written in valid JS.

Typing props with interface or type

A component's props are an object, so we describe their shape with an interface (or a type). Then we use it to type the parameter:

interface GreetingProps {
  name: string;
  age?: number; // the "?" makes it optional
}

function Greeting({ name, age }: GreetingProps) {
  return <h1>Hi, {name}{age ? ` (${age})` : ""}</h1>;
}

interface and type are almost interchangeable for props; use whichever your team prefers. type also allows unions (type State = "ok" | "error").

Components: typed function or React.FC

The recommended approach today is a plain function with typed props (like above). There's also React.FC, which types the whole component and includes children implicitly in old versions:

const Button: React.FC<{ text: string }> = ({ text }) => {
  return <button>{text}</button>;
};

In modern React, the typed function is preferred over React.FC, because it's more explicit (you declare children only if you need it) and more flexible.

Typed state: useState<T>

useState infers the type from the initial value:

const [name, setName] = useState("");       // string
const [active, setActive] = useState(false); // boolean

When the initial value isn't enough to infer (e.g. it starts as null but will later store an object), you annotate it explicitly with useState<T>:

interface User { id: number; name: string; }

const [user, setUser] = useState<User | null>(null);

Typing events

Event handlers receive a React event, not a native DOM one. The types live in the React.* namespace:

function Field() {
  const [text, setText] = useState("");

  function onType(e: React.ChangeEvent<HTMLInputElement>) {
    setText(e.target.value);
  }

  function onSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
  }

  return (
    <form onSubmit={onSubmit}>
      <input value={text} onChange={onType} />
    </form>
  );
}

The most common ones: React.ChangeEvent<HTMLInputElement> (inputs), React.MouseEvent<HTMLButtonElement> (clicks), React.FormEvent (forms), and React.KeyboardEvent (keyboard). If you write the handler inline (onClick={(e) => ...}), TypeScript usually infers the event type for you.

children and React.ReactNode

children is "whatever goes between the opening and closing tags" of the component. Since it can be text, a number, a JSX element, a list of them, or nothing, its type is React.ReactNode: the most general type for "anything React can render".

interface CardProps {
  title: string;
  children: React.ReactNode;
}

function Card({ title, children }: CardProps) {
  return (
    <section>
      <h2>{title}</h2>
      <div>{children}</div>
    </section>
  );
}

// Usage:
<Card title="Hi">
  <p>Any content goes here.</p>
</Card>

Mental rule: use React.ReactNode for children and for any prop that receives "renderable content". For a prop that is one specific element, there's React.ReactElement.

Put this into practice

DevPath is a hands-on course: you read the theory here; in the app you put it into practice with exercises that really run, offline.

Start free in the app →
Server rendering: CSR, SSR, SSG, and Server Components →