Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Typing Component Props with Optional Values and Callbacks

Welcome back. Last lesson, you used a CounterProps type to require a label and saw that each counter keeps its own state. Now you’ll make that contract more useful: a component can accept an optional value and a callback function as well as a required string. This is how a child component can display information supplied by its parent and report an action back to it.

Allow about 40 minutes, including the resources and changes to your Vite project.


A props type describes the component’s contract

Your earlier component began with a one-field object type:

type CounterProps = {
  label: string
}

The type describes what a parent must supply at each <Counter /> call site. It also tells TypeScript what label means inside the component. You can use either a type or an interface for an ordinary props object; we’ll keep using type for consistency with your existing code.

Use the React TypeScript Cheatsheets reference to scan the syntax you’ll need. You do not need to adopt every prop type in its list.

Typing Component Props - React TypeScript Cheatsheets

Read “Typing Component Props” from React TypeScript Cheatsheets as a compact reference for required values, function props, and optional props.

In “Basic Prop Types Examples,” start at the sentence “A list of TypeScript types you will likely use in a React+TypeScript app.” Read the prop examples through the optional?: OptionalType; declaration immediately after the optional-prop comment. Skim the object and dictionary examples; focus on the string and number fields, the onClick and onChange function signatures, and the optional-prop declaration. Stop before “object as the non-primitive type.”

A name such as onCountChange does not have special powers in React. It is an ordinary prop whose value happens to be a function. Its signature specifies which arguments the child may pass when it calls that function.


Optional means the parent may omit it

Suppose a counter can show a short description, but not every counter needs one. Put ? after the property name:

type CounterProps = {
  label: string
  description?: string
}

label is required; description is optional. Inside the component, description might therefore be undefined. That is different from saying its value can be any type: a parent can omit it or pass a string, but cannot pass a number.

Watch how an optional property affects both a component call and the type visible inside the component:

React Course - TypeScript - Props

Watch “React Course - TypeScript - Props” by Techbase for its demonstration of optional props and fallback values.

In optional props, notice the error when a newly required prop is missing and the string | undefined type after ? is added. In default values, focus on how a supplied value replaces the fallback. The video uses a different project structure; apply the syntax to your CounterProps rather than copying its files.

One way to handle omission is to give the destructured prop a default:

function Counter({
  label,
  description = 'No description provided',
}: CounterProps) {
  return (
    <section>
      <h2>{label}</h2>
      <p>{description}</p>
    </section>
  )
}

Within this function, description is now a string. The default applies when the prop is omitted or supplied as undefined; it does not replace an intentionally supplied empty string. A default is a display decision, not a change to the parent’s obligation: description remains optional in CounterProps.


A callback lets the child report an action

Imagine the parent wants to display which counter was most recently increased. The counter owns its count, so the parent cannot read that local state directly. Instead, the parent can give it a function to call when an increase happens.

The corresponding prop type is:

onCountChange: (label: string, nextCount: number) => void

This says the child can call onCountChange with a string and a number. void says the child does not need a result from the function. The names label and nextCount document what the arguments mean; their types and order form the contract.

Watch the callback-prop portion of TypeScript in React - Full Tutorial:

TypeScript in React - Full Tutorial

Watch “TypeScript in React - Full Tutorial” by freeCodeCamp.org for the distinction between passing a value prop and passing a function prop.

In callback props, follow the function-signature syntax. The example starts with a callback that takes no arguments, then briefly shows one with an argument. You can set aside its explicit component return-type annotation; your project can continue to infer that type.

The parent passes a function with onCountChange={handleCountChange}. The child calls that function when the action occurs. Writing onCountChange={handleCountChange()} at the call site would instead call it while rendering and attempt to pass its result—not the function—as the prop.


Extend your two-counter page

Replace src/App.tsx with the following. This retains the two independent counters from last lesson, adds an optional description, and gives both counters the same required callback.

import { useState } from 'react'

type CounterProps = {
  label: string
  description?: string
  onCountChange: (label: string, nextCount: number) => void
}

function Counter({
  label,
  description = 'No description provided',
  onCountChange,
}: CounterProps) {
  const [count, setCount] = useState(0)

  function handleAddOne() {
    const nextCount = count + 1
    setCount(nextCount)
    onCountChange(label, nextCount)
  }

  return (
    <section>
      <h2>{label}</h2>
      <p>{description}</p>
      <p>Count: {count}</p>
      <button type="button" onClick={handleAddOne}>
        Add one to {label}
      </button>
    </section>
  )
}

function App() {
  const [lastUpdate, setLastUpdate] = useState('None yet')

  function handleCountChange(label: string, nextCount: number) {
    setLastUpdate(`${label}: ${nextCount}`)
  }

  return (
    <main>
      <h1>Activity</h1>
      <p>Last update: {lastUpdate}</p>

      <Counter
        label="User activity"
        description="Actions in the user area"
        onCountChange={handleCountChange}
      />
      <Counter
        label="Other activity"
        onCountChange={handleCountChange}
      />
    </main>
  )
}

export default App

Follow one click through the code. handleAddOne computes the next count, requests a state update for that particular counter, and calls the parent-supplied function with the label and new number. App’s handler uses those arguments to update its “Last update” display. It does not reach into the child to retrieve its state. The two counters still own separate counts, while the parent owns the record of the latest reported action.

Notice also why the callback receives nextCount rather than reading count after setCount(nextCount): calling a setter requests another render; it does not change the count variable already captured by this handler.


Check the contract at the call site

Run the page and click each button a few times. Each count should change independently, and “Last update” should reflect the most recent click. Then run the type check you used previously:

npm run typecheck

To see what the contract protects, temporarily make one of these changes to the second <Counter /> call and run the type check again:

  • Omit onCountChange: TypeScript should report that a required prop is missing.
  • Add description={42}: TypeScript should report that a number is not a valid description.
  • Replace the callback with onCountChange={(label: number) => console.log(label)}: its first parameter has the wrong type. A callback may ignore arguments it does not need, but any argument it declares must be compatible with the signature.

Restore the working call afterward. These errors are caught where the parent uses the component, before someone has to discover the mismatch by clicking through the interface.

A callback can itself be optional when a component genuinely works without a listener. In that case, its type would be onCountChange?: (label: string, nextCount: number) => void, and the child could call it safely with onCountChange?.(label, nextCount). Keep it required in the current example: reporting updates is part of this counter’s intended behavior.


What to carry forward

A props type is the agreement between a component and its callers. Required fields must be supplied; optional fields may be absent and need a deliberate fallback or check; callback fields specify what information the child can send to a function supplied by its parent. TypeScript checks that agreement, while React renders the component and responds to the state updates and clicks.

Next, you’ll work directly with browser events and local state. The button here needs no event details, but inputs and other controls often do—and their handlers need appropriate types.

Can't find a good explanation? Sign up and we'll make it for you

Sign up