Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Typing Components and Props in React with TypeScript

Typing Components and Props

~15 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026

Now we'll migrate ProductCard – the most frequently reused component in the project, and a perfect example of how TypeScript prevents exactly the errors that arise when passing props across multiple files.

ProductCard.jsx to ProductCard.tsx: the props interface

src/components/ProductCard.tsx
import { memo, useState } from 'react';

interface ProductCardProps {
  name: string;
  price: number;
  imageUrl: string;
  onSelect: () => void;
  onAddToCart: () => void;
}

function ProductCard({ name, price, imageUrl, onSelect, onAddToCart }: ProductCardProps) {
  const [isFavorite, setIsFavorite] = useState(false);

  function handleFavoriteClick(event: React.MouseEvent) {
    event.stopPropagation();
    setIsFavorite(!isFavorite);
  }

  function handleAddToCartClick(event: React.MouseEvent) {
    event.stopPropagation();
    onAddToCart();
  }

  return (
    <div className="product-card" onClick={onSelect}>
      <img src={imageUrl} alt={name} className="product-card__image" />
      <div className="product-card__info">
        <h3 className="product-card__name">{name}</h3>
        <p className="product-card__price">${price.toFixed(2)}</p>
      </div>
      <button className="product-card__favorite" onClick={handleFavoriteClick}>
        {isFavorite ? '♥' : '♡'}
      </button>
      <button className="product-card__add-to-cart" onClick={handleAddToCartClick}>
        Add to Cart
      </button>
    </div>
  );
}

export default memo(ProductCard);

The details that matter

  • interface ProductCardProps describes the FULL public interface of the component – anyone reading the file sees IMMEDIATELY (without reading the whole function body) which props are required and what type they are.
  • onSelect: () => void – the arrow syntax inside an interface describes a FUNCTION SIGNATURE: "a function that takes no arguments and returns nothing" (void, not undefined – the difference is covered in the TypeScript language tutorial).
  • event: React.MouseEvent – the type for a synthetic React mouse event; without this annotation, TypeScript would implicitly treat event as any, which disables ALL type checking on it.
  • memo(ProductCard) at the very end works UNCHANGED – TypeScript's type inference automatically recognizes that the result is still a component with the same props.

Experiencing the benefit live: a deliberate typo

Open ProductListPage.jsx (not yet migrated) and, as an experiment, change onAddToCart={{...}} to onAddToCard={{...}} (swapped "r"/"d"). Since ProductListPage.jsx itself is still JavaScript, your editor won't report anything HERE yet – instead, try changing ONE <ProductCard ... /> call in a .tsx file to have that typo: your editor underlines onAddToCard in RED and shows "Property 'onAddToCard' does not exist on type 'ProductCardProps'" – BEFORE running anything, not only when click-testing in the browser.

Bonus: typing children (for later components)

For components with children (like our Modal from chapter 38), there's a built-in React type:

import { ReactNode } from 'react';

interface ModalProps {
  isOpen: boolean;
  onClose: () => void;
  children: ReactNode;
}

ReactNode is the broadest "anything React can render" type (strings, numbers, JSX elements, arrays thereof, null, ...) – the right type for children when you don't want to restrict what can be passed in.

Optional props with the question mark

Remember size = 48 as a default-value pattern from the "React Native Reference" series (the Avatar topic)? The TypeScript equivalent of "this prop is optional" is a ? in the interface:

interface AvatarProps {
  name: string;
  imageUrl?: string; // optional - can be omitted
  size?: number;     // optional, with a default value in the function header
}

function Avatar({ name, imageUrl, size = 48 }: AvatarProps) {
  // ...
}

Tipp: Rule of thumb for migration order in a real project: "inside out", or "leaves to roots" – small, reusable components WITHOUT many dependencies (like ProductCard) first, complex page components with lots of imports (ProductListPage) last. That keeps the rest of the app runnable throughout the gradual migration.