Skip to main content
    Courses/TypeScript/TypeScript with React

    Lesson 7 • Advanced

    TypeScript with React ⚛️

    By the end of this lesson you'll be able to type a React component's props, its useState and useRef, and its event handlers — so your editor catches mistakes as you type instead of your users finding them at runtime.

    What You'll Learn

    • Type component props with an interface (or a type alias) — and know when to use each
    • Type useState<T>() — and why an empty array needs an explicit generic
    • Type useRef<T>() for DOM elements and for plain mutable values
    • Type event handlers: React.ChangeEvent<HTMLInputElement> and React.MouseEvent
    • Accept nested content with the children type React.ReactNode
    • Understand React.FC — and why many teams now avoid it

    💡 Real-World Analogy

    A component's props interface is the label on a shipping box: "Contains: 1 string (name), 1 number (age), optional VIP sticker." Anyone sending you a box must match that label, or the post office rejects it at the counter — before it ships. Plain React lets any box through and you discover the missing parts after delivery (at runtime). TypeScript checks the label at the counter, so wrong or missing props never reach your users.

    1. Typing Component Props

    Every React component receives a single props object. To type it, you describe its shape once with an interface, then annotate the parameter with that interface. From then on TypeScript enforces required props, rejects wrong types, and autocompletes prop names for you. A ? after a prop name makes it optional. Read this worked example carefully.

    // Greeting.tsx — a component that takes typed props.
    
    // 1) Describe the SHAPE of the props with an interface.
    //    Each line is: propName: type;   add ? to make it optional.
    interface GreetingProps {
      name: string;          // required text
      age: number;           // required number
      isVip?: boolean;       // optional — note the "?"
    }
    
    // 2) Annotate the props parameter with that interface.
    //    Now TypeScript knows name is a string, age is a number, etc.
    function Greeting({ name, age, isVip = false }: GreetingProps) {
      return (
        <p>
          Hi {name}, you are {age}.
          {isVip ? " ⭐ VIP" : ""}
        </p>
      );
    }
    
    // 3) Using it — TypeScript checks every prop you pass:
    //    <Greeting name="Sam" age={20} />            ✅ valid
    //    <Greeting name="Sam" age={20} isVip />      ✅ optional prop supplied
    //    <Greeting name="Sam" />                     ❌ Error: 'age' is missing
    //    <Greeting name="Sam" age="20" />            ❌ Error: string not assignable to number

    You can describe props with either an interface or a type alias — for prop shapes they're nearly identical. A common convention: use interface for object shapes you might extend, and type for unions and one-offs. Notice the "primary" | "danger" union — that prop can only be one of those two strings.

    // You can describe props with an INTERFACE or a TYPE alias.
    // For component props they are almost interchangeable — pick one and be consistent.
    
    interface ButtonProps {            //  <-- interface
      label: string;
      onClick: () => void;             //  a function that takes nothing, returns nothing
      variant?: "primary" | "danger";  //  union of allowed strings (only these two!)
    }
    
    type CardProps = {                 //  <-- type alias, same idea
      title: string;
      footer?: string;
    };
    
    // Rule of thumb:
    //   interface  → object/prop shapes you might extend later (most components)
    //   type       → unions, intersections, and one-off shapes
    //
    //   variant?: "primary" | "danger"  means the value can ONLY be one of those
    //   two strings. Pass variant="blue" and TypeScript stops you before runtime.

    🎯 Your Turn: finish the props interface

    Fill in each ___ below. This snippet is read-only — work it out in your head (or type it into a real .tsx file) and check against the // ✅ Expected comments.

    // 🎯 YOUR TURN — finish this props interface, then read the checklist below.
    
    interface ProfileCardProps {
      // 1) "username" must be text
      username: ___;          // 👉 the type for text
      // ✅ Expected: username: string;
    
      // 2) "score" must be a number
      score: ___;             // 👉 the type for whole/decimal numbers
      // ✅ Expected: score: number;
    
      // 3) "showBadge" is OPTIONAL and true/false
      ___ : boolean;          // 👉 add the property name with a "?" to mark it optional
      // ✅ Expected: showBadge?: boolean;
    }
    
    function ProfileCard({ username, score, showBadge = false }: ProfileCardProps) {
      return <div>{username} — {score}{showBadge ? " 🏅" : ""}</div>;
    }

    2. Typing useState & useRef

    Both hooks are generic — they take a type inside angle brackets, like useState<number>(). Most of the time you don't need to write it: useState(0) is inferred as a number from its initial value. You add an explicit <T> when the initial value can't describe the real type — an empty array, or a value that's null now but will hold an object later.

    // useState<T>() and useRef<T>() — telling React what type lives inside.
    
    // A) useState INFERS the type from the initial value — no generic needed:
    const [count, setCount] = useState(0);      // inferred as number
    const [name, setName]   = useState("");     // inferred as string
    setCount(5);        // ✅ ok
    // setCount("five"); // ❌ Error: string not assignable to number
    
    // B) Add an explicit <T> when the initial value can't describe the type.
    //    Empty array? Tell it WHAT the array holds, or it becomes never[]:
    const [tags, setTags] = useState<string[]>([]);   // an array of strings
    setTags(["react", "ts"]);   // ✅
    
    // C) Nullable state — value starts null but will become a User later.
    //    The <User | null> generic is what makes the null-check below necessary.
    interface User { id: number; name: string; }
    const [user, setUser] = useState<User | null>(null);
    // user.name        ❌ Error: 'user' is possibly null
    if (user) {
      console.log(user.name);   // ✅ inside the guard, user is User (not null)
    }
    
    // D) useRef<T>() — a "box" that survives re-renders.
    //    For a DOM element, type it with the element + start it at null:
    const inputRef = useRef<HTMLInputElement>(null);
    inputRef.current?.focus();   // ?. because current is null until React attaches it
    
    //    For a plain mutable value, give the type and an initial value:
    const renderCount = useRef<number>(0);
    renderCount.current += 1;    // changing this does NOT trigger a re-render

    The never[] trap

    Write const [items, setItems] = useState([]); and TypeScript infers the type as never[] — "an array that can hold nothing." The moment you call setItems(["a"]) you get "Argument of type string is not assignable to parameter of type never."

    The fix is to tell it what the array holds: useState<string[]>([]). This is the single most common state-typing mistake.

    🎯 Your Turn: type three pieces of state

    Replace each ___ with the correct generic. Check yourself against the // ✅ Expected lines.

    // 🎯 YOUR TURN — give each piece of state the right type.
    
    // 1) A list of numbers that starts empty.
    //    An empty [] alone would be inferred as never[] — so add the generic!
    const [scores, setScores] = useState<___>([]);   // 👉 array of numbers
    // ✅ Expected: useState<number[]>([])
    
    // 2) A value that is a Product OR null until it loads.
    interface Product { id: number; title: string; }
    const [product, setProduct] = useState<___>(null);  // 👉 Product OR null
    // ✅ Expected: useState<Product | null>(null)
    
    // 3) A ref to a <div> element on the page.
    const boxRef = useRef<___>(null);   // 👉 the DOM type for a div element
    // ✅ Expected: useRef<HTMLDivElement>(null)

    3. Typing Events & Children

    React gives each event a specific type, and you tell it which HTML element fired it using a generic. Type a change handler as React.ChangeEvent<HTMLInputElement> and e.target.value is correctly known to be a string. A click handler is React.MouseEvent<HTMLButtonElement>. To accept nested content (anything between your component's tags), type the children prop as React.ReactNode — the catch-all for "anything renderable."

    // Typing event handlers — the event object carries the right element type.
    
    // Change event on an <input>. Because we typed it HTMLInputElement,
    // e.target.value is known to be a string and autocompletes.
    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
      setName(e.target.value);     // ✅ value is a string
    };
    
    // Click event on a <button>:
    const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
      e.preventDefault();
      console.log("clicked at", e.clientX, e.clientY);
    };
    
    // Wiring them up in JSX:
    // <input onChange={handleChange} />
    // <button onClick={handleClick}>Save</button>
    
    // Children — accept ANY renderable content (text, JSX, numbers, arrays)
    // with React.ReactNode. This is the type to reach for whenever a component
    // wraps other content.
    interface CardProps {
      title: string;
      children: React.ReactNode;   // <-- anything you can render
    }
    
    function Card({ title, children }: CardProps) {
      return (
        <section>
          <h2>{title}</h2>
          <div>{children}</div>     {/* whatever was nested inside <Card> */}
        </section>
      );
    }
    
    // <Card title="Hi"><p>Some content</p></Card>   ✅ <p> is a valid ReactNode

    4. Run It: the immutable state-update pattern

    The TSX above can't run in this editor, but the update pattern you'll use with a typed setState<Todo[]> is plain JavaScript. The golden rule: never mutate the existing state — build and return a brand-new value. This runnable demo simulates setTodos with a function so you can press Run and watch it work.

    🎯 Run It: immutable todo updates

    The new-array pattern behind every typed setState call

    Try it Yourself »
    JavaScript
    // This runs in the editor (plain JS) to show the IMMUTABLE update pattern
    // you'll use with React's typed setState. In real React + TS you'd write:
    //   const [todos, setTodos] = useState<Todo[]>([]);
    // Here we simulate setTodos with a plain function so you can run it.
    
    let todos = [];   // pretend this is state: an array of { id, text, done }
    
    // setState replaces the value — never mutate the old array, build a NEW one.
    function addTodo(list, text) {
      return [...list, { id: list.length + 1,
    ...

    🔎 Deep Dive: React.FC — and why many avoid it

    You'll meet React.FC (short for "Function Component") in older code and tutorials. It works, but most teams have moved away from it in favour of typing the props directly on a plain function.

    // You will see React.FC ("Function Component") in older tutorials:
    const Hello: React.FC<{ name: string }> = ({ name }) => <p>Hi {name}</p>;
    
    // It works, but most teams now AVOID it. The plain version is preferred:
    function Hello({ name }: { name: string }) {
      return <p>Hi {name}</p>;
    }
    
    // Why many avoid React.FC:
    //  • It used to add an implicit "children" prop even when a component
    //    takes no children — easy to pass children by accident.
    //  • It makes generic components awkward to write.
    //  • The plain "function Component(props: Props)" form is simpler and
    //    is what the React docs themselves use today.
    // Bottom line: type the props directly; reach for React.FC only if a
    // codebase already standardises on it.

    Common Errors (and the fix)

    • Untyped props — "Parameter 'props' implicitly has an 'any' type": you wrote function Greeting(props) { with no type. Add an interface and annotate it: function Greeting(props: GreetingProps).
    • Wrong event type — "Property 'value' does not exist on type EventTarget": you typed a change handler as a generic Event. Use React.ChangeEvent<HTMLInputElement> so e.target.value exists.
    • useState inferred as never"Argument of type 'string' is not assignable to parameter of type 'never'": you started state at []. Add the generic: useState<string[]>([]).
    • Reading nullable state — "Object is possibly 'null'": state typed User | null must be guarded. Wrap access in if (user) { ... } or use optional chaining user?.name.
    • Ref used too early — "Object is possibly 'null'" on inputRef.current: it's null until React attaches the element. Access it with inputRef.current?.focus().

    📋 Quick Reference

    What you're typingCode
    Component propsfunction C(props: Props) { ... }
    Optional propisVip?: boolean;
    State (inferred)useState(0)
    State (array)useState<string[]>([])
    State (nullable)useState<User | null>(null)
    DOM refuseRef<HTMLInputElement>(null)
    Change eventReact.ChangeEvent<HTMLInputElement>
    Click eventReact.MouseEvent<HTMLButtonElement>
    Childrenchildren: React.ReactNode

    Frequently Asked Questions

    Q: Should I use interface or type for props?

    For props they're almost interchangeable. A common convention: interface for object/prop shapes (it can be extended later), and type for unions, intersections, and one-offs. Pick one style and stay consistent within a project.

    Q: Do I always have to write the generic on useState?

    No — TypeScript infers it from the initial value, so useState(0) is a number automatically. Add the explicit <T> only when the initial value can't describe the real type: empty arrays (useState<string[]>([])) and nullable values (useState<User | null>(null)).

    Q: Why does e.target.value sometimes error?

    Because the event wasn't typed to a specific element. A plain Event has no value. Type the handler React.ChangeEvent<HTMLInputElement> and TypeScript knows e.target is an input, so .value appears.

    Q: Is React.FC wrong?

    Not wrong — just out of fashion. It used to silently add a children prop and makes generic components awkward. The React docs now show plain functions with directly-typed props, so prefer function C(props: Props) unless your codebase already standardises on FC.

    Mini-Challenge: a typed Counter

    No blanks this time — just a brief and an outline. Build it in a real react-ts project, then use the self-check at the bottom to confirm your types are right.

    // 🎯 MINI-CHALLENGE: a typed "Counter" component
    //
    // Build a <Counter /> component (write the types yourself — no blanks given):
    // 1. Props interface "CounterProps":
    //      - "start" : a number (the initial count)
    //      - "label" : a string
    //      - "onChange?" : OPTIONAL — a function (newValue: number) => void
    // 2. Inside, hold the count with useState, typed/inferred from "start".
    // 3. Add a handleClick typed as React.MouseEvent<HTMLButtonElement>
    //    that increases the count and calls onChange (if provided).
    // 4. Render: <button>{label}: {count}</button>
    //
    // ✅ Self-check: <Counter start={0} label="Clicks" /> compiles, and
    //    <Counter start="0" label="Clicks" /> is a TYPE ERROR (string not number).
    
    // your code here

    Pro Tips

    • 💡 Let inference do the work: only add an explicit <T> when the initial value can't describe the type.
    • 💡 Hover, don't memorise: in VS Code, hover over onChange on any JSX element to see the exact event type it expects.
    • 💡 Type the children prop as React.ReactNode for any wrapper component — it accepts text, JSX, numbers, and arrays.
    • 💡 Prefer plain typed functions over React.FC for new components.

    🎉 Lesson Complete

    • ✅ Describe props with an interface (or type) and annotate the props parameter
    • useState usually infers; add <T> for empty arrays and nullable values
    • useRef<HTMLInputElement>(null) for DOM, useRef<number>(0) for mutable values
    • ✅ Event handlers carry their element type: React.ChangeEvent<HTMLInputElement>, React.MouseEvent
    • children: React.ReactNode accepts any renderable content
    • ✅ Prefer plain typed functions over React.FC
    • Next lesson: TypeScript Best Practices for production codebases

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service