TypeScript with React

Reviewed & published by Brayan K

TypeScript adds static types to JavaScript so mistakes are caught while you write code, not when a user hits a bug. In React, that means typing your props, state, and event handlers — the editor autocompletes them and the compiler refuses to ship a wrong shape. By the end you'll type a component end to end and read the most common React+TS patterns with confidence.

Part of the free React course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

1️⃣ Typing Props

The single most valuable thing TypeScript does for React: describe the shape of a component's props. Declare a type (or interface) and annotate the props parameter. Now every place that renders the component is checked — pass the wrong type or forget a required prop and you get a red squiggle immediately.

type ButtonProps = {
  label: string;
  onClick: () => void;     // a function returning nothing
  disabled?: boolean;      // optional
};

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

// ✅ ok:    <Button label="Save" onClick={save} />
// ❌ error: <Button label={42} />   // 42 is not a string; missing onClick

The demo runs a typed function so you can see required and optional fields in action (the types are real TS in the block above):

// TypeScript lets you DESCRIBE the shape of your props so mistakes are
// caught before the app runs. Define a type, then annotate the parameter.

type ButtonProps = {
  label: string;          // required string
  count: number;          // required number
  disabled?: boolean;     // optional (the ? means "may be missing")
};

// Annotate the props object with the type:
function describeButton({ label, count, disabled = false }: ButtonProps): string {
  const state = disabled ? "disabled" : "enabled";
  return `[${label}] count=${count} (${state})`;
}

console.log(describeButton({ label: "Save", count: 3 }));
// [Save] count=3 (enabled)

console.log(describeButton({ label: "Delete", count: 0, disabled: true }));
// [Delete] count=0 (disabled)
// Pass the wrong type (e.g. count: "3") and TypeScript flags it at compile time.

Your turn — fill in the type annotations so only valid data is accepted:

// 🎯 YOUR TURN — fill in each ___ then run it.
// Type this greet function's props so only valid data is accepted.

type UserProps = {
  name: ___;        // 👉 string  (the user's name)
  age: ___;         // 👉 number  (the user's age)
};

function greet({ name, age }: UserProps): string {
  return `${name} is ${age}`;
}

console.log(greet({ name: "Ada", age: 36 }));
console.log(greet({ name: "Linus", age: 54 }));

// ✅ Expected output:
//    Ada is 36
//    Linus is 54

2️⃣ Typing State

Most of the time TypeScript infers state from the initial value: useState(0) is a number, useState("") is a string. You only annotate when the initial value doesn't tell the whole story — like null at first, or a union of options.

// Inference is enough — count is number, name is string:
const [count, setCount] = useState(0);
const [name, setName] = useState("");

// Annotate when the initial value is null or you need a union:
const [user, setUser] = useState<User | null>(null);
const [status, setStatus] = useState<"idle" | "loading" | "done">("idle");

Union types shine for status flags and variants — the demo shows a Status union that makes typos impossible:

// Typing STATE and a discriminated union. A "Status" can only be one of
// three exact strings — TypeScript stops you from typo-ing "lodaing".

type Status = "idle" | "loading" | "success";   // a union of literal types

function label(status: Status): string {
  switch (status) {
    case "idle":    return "Ready";
    case "loading": return "Please wait…";
    case "success": return "Done!";
  }
}

console.log(label("idle"));      // Ready
console.log(label("loading"));   // Please wait…
console.log(label("success"));   // Done!
// label("loadng") would be a COMPILE ERROR — the typo can't reach production.

3️⃣ Events & Children

Two patterns you'll use constantly. Type an event handler with React's built-in event types so e.target.value is known. Type children with React.ReactNode — the catch-all for anything renderable.

// Event handler: React types the event so e.target.value is a string.
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
  console.log(e.target.value);
}

// Children: ReactNode covers JSX, strings, numbers, arrays, null...
type CardProps = { title: string; children: React.ReactNode };

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

A handy rule: for a value that returns nothing, type it () => void; for anything you can render, reach for React.ReactNode.

Reorder Challenge

These lines declare a typed component. Put them in a correct order:

A.   return <button onClick={onClick}>{label}</button>;
B.   type Props = { label: string; onClick: () => void };
C.   function Button({ label, onClick }: Props) {
D.   }

Quick Recall

1) What does the ? in disabled?: boolean mean?

The prop is optional — it may be omitted. Inside the component its type is boolean | undefined.

2) Does useState(0) need a type annotation?

No. TypeScript infers number from the initial value. Annotate only for null starts or unions, e.g. useState<User | null>(null).

3) Which type covers "anything renderable" for the children prop?

React.ReactNode — it covers JSX, strings, numbers, arrays, null, and more.

Common Errors (and the fix)

📋 Quick Reference

GoalSyntax
Props typetype Props = { label: string };
Annotate propsfunction C({ label }: Props) {}
Optional propdisabled?: boolean
State with unionuseState<"a" | "b">("a")
Event handler(e: React.ChangeEvent<HTMLInputElement>) => {}
Childrenchildren: React.ReactNode

Mini-Challenge: Type a Badge

Write a typed function that formats a badge. Type count as a number and an optional tone union, then return a label. Predict the output for the calls below.

🎉 Lesson Complete

Practice quiz

How do you type a component's props in modern React + TypeScript?

  • Add PropTypes at the bottom of the file
  • Wrap the component in a generic
  • Define a type or interface for the props and annotate the props parameter
  • Props can't be typed

Answer: Define a type or interface for the props and annotate the props parameter. Declare a type/interface (e.g. ButtonProps) and annotate the parameter: function Button(props: ButtonProps). Every usage is then checked.

What does the ? in disabled?: boolean mean?

  • The prop is optional — it may be omitted, and its type becomes boolean | undefined
  • The prop is private
  • The prop defaults to true
  • The prop is nullable but required

Answer: The prop is optional — it may be omitted, and its type becomes boolean | undefined. A ? marks the prop optional. Callers may omit it, and inside the component its type includes undefined.

Does useState(0) need an explicit type annotation?

  • Yes, always annotate useState
  • Only in strict mode
  • Yes, or it defaults to any
  • No — TypeScript infers number from the initial value

Answer: No — TypeScript infers number from the initial value. Inference handles primitives: useState(0) is number, useState('') is string. You annotate only when the initial value doesn't tell the whole story.

When do you need to annotate useState with a generic?

  • Never
  • When the initial value is null or you need a union type, e.g. useState<User | null>(null)
  • Only for numbers
  • Whenever the state is an object

Answer: When the initial value is null or you need a union type, e.g. useState<User | null>(null). A null start or a union of options can't be inferred fully, so you supply the generic: useState<User | null>(null) or useState<'idle' | 'done'>('idle').

What is a good use for union types like 'idle' | 'loading' | 'success'?

  • Constraining a status to a fixed set of exact strings so typos become compile errors
  • Counting renders
  • Replacing useState
  • Typing event handlers

Answer: Constraining a status to a fixed set of exact strings so typos become compile errors. A literal union restricts a value to specific strings. Mistyping 'lodaing' won't compile, catching the bug before runtime.

How should you type the children prop?

  • string
  • any
  • React.ReactNode
  • JSX.Element only

Answer: React.ReactNode. React.ReactNode is the catch-all for anything renderable: JSX, strings, numbers, arrays, null, and more.

How do you type an input's change handler so e.target.value is known?

  • (e: any) => void
  • (e: React.ChangeEvent<HTMLInputElement>) => void
  • (e: Event) => void
  • (e: string) => void

Answer: (e: React.ChangeEvent<HTMLInputElement>) => void. React's built-in event types describe the event. React.ChangeEvent<HTMLInputElement> makes e.target.value a known string.

What type best describes a callback prop that returns nothing, like onClick?

  • () => null
  • Function
  • () => undefined required
  • () => void

Answer: () => void. () => void types a function that takes no arguments and returns nothing — the standard shape for event-style callbacks.

Is React.FC the recommended way to type a component today?

  • Yes, always use React.FC
  • Not really — annotating the props object directly is the recommended modern style; React.FC adds quirks
  • React.FC is required for hooks
  • React.FC is the only option in strict mode

Answer: Not really — annotating the props object directly is the recommended modern style; React.FC adds quirks. Many teams skip React.FC because it adds an implicit children prop and some quirks. Annotating props directly (function Comp(props: Props)) is preferred.

You get "Property 'onClick' is missing in type". What does it mean?

  • onClick is misspelled in React
  • TypeScript is broken
  • You forgot a required prop when rendering the component — add it or mark it optional with ?
  • onClick must be a string

Answer: You forgot a required prop when rendering the component — add it or mark it optional with ?. The error flags a missing required prop. Either pass it, or mark it optional with ? if it genuinely is.

Continue this course

Related lessons