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

Typing Events, Forms, and API Data with TypeScript

Typing Events, Forms, and API Data

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

To wrap up the TypeScript integration: the three spots where TypeScript prevents the most bugs – form events (the notorious "event.target.value is of type any" problems), and data coming from OUTSIDE the app (the API), where TypeScript naturally knows NOTHING about the actual structure until you tell it.

LoginPage.tsx: typing form events

src/pages/LoginPage.tsx
import { useState, FormEvent, ChangeEvent } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from '../store/authStore';

function LoginPage() {
  const login = useAuthStore((state) => state.login);
  const [username, setUsername] = useState('');
  const navigate = useNavigate();

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    login(username || 'Jane Doe');
    navigate('/account');
  }

  function handleUsernameChange(event: ChangeEvent<HTMLInputElement>) {
    setUsername(event.target.value);
  }

  return (
    <form onSubmit={handleSubmit}>
      <h2>Log In</h2>
      <input
        type="text"
        placeholder="Username"
        value={username}
        onChange={handleUsernameChange}
      />
      <button type="submit">Log In</button>
    </form>
  );
}

export default LoginPage;

FormEvent<HTMLFormElement> and ChangeEvent<HTMLInputElement> are React's generic event types, PARAMETERIZED with the concrete DOM element type – EXACTLY why TypeScript knows, for event.target.value, that event.target is an HTMLInputElement (with a .value property of type string), not the generic, far less-typed EventTarget. WITHOUT the generic, event.target.value would throw a type error – EventTarget itself has NO .value field.

The API boundary: typing magentoApi.ts

The most important, but most easily overlooked point: TypeScript only checks code IT can itself analyze. A fetch() response from an external server is a pure ASSERTION at COMPILE time – TypeScript trusts you when you specify the shape of the response, but does NOT check at runtime whether the server actually delivers what you promised.

src/api/magentoApi.ts
import type { CartItem } from '../store/cartSlice';

export interface Product {
  sku: string;
  name: string;
  price: number;
  imageUrl: string;
}

interface ProductsResponse {
  items: Product[];
  totalCount: number;
}

interface MagentoApiItem {
  sku: string;
  name: string;
  price: number;
  image?: string;
}

function mapMagentoProduct(item: MagentoApiItem): Product {
  return {
    sku: item.sku,
    name: item.name,
    price: item.price,
    imageUrl: item.image ?? 'https://picsum.photos/300',
  };
}

export async function fetchProducts(page: number, pageSize: number): Promise<ProductsResponse> {
  const response = await fetch(
    `https://api.example.com/products?page=${page}&pageSize=${pageSize}`
  );

  if (!response.ok) {
    throw new Error(`API error: ${response.status}`);
  }

  const data = await response.json(); // type here: 'any' - see warning below
  return {
    items: data.products.map(mapMagentoProduct),
    totalCount: data.total,
  };
}

Achtung: await response.json() ALWAYS returns any – TypeScript simply cannot know what a remote server actually sends. The function's Promise<ProductsResponse> return type is an ASSERTION on your part, not an automatically verified guarantee – TypeScript only checks that YOUR OWN code stays internally CONSISTENT with that assertion, not that the server honors it. For GENUINE runtime validation, you'd need an additional library like zod, which actually checks the response against a schema at runtime – beyond the scope of this chapter, but an important next step for production apps.

Why this is worth it anyway

Even WITHOUT runtime validation, typing the API layer brings enormous value: EVERY caller of fetchProducts() in the rest of the code now gets back a correctly typed Product[] list – typos like product.pric (instead of product.price) get caught IMMEDIATELY, EVERYWHERE fetchProducts() is used, not only once the affected line actually runs.

Reusing CartItem from chapter 40

Notice import type {{ CartItem }} from '../store/cartSlice' at the top of magentoApi.ts – we import the type defined in chapter 40 instead of reinventing it. That's exactly the point of export interface CartItem: ONE single source of truth for "what a cart item is", importable from ANY file in the project.

Tipp: That completes the core project's migration to TypeScript: cartSlice, store/index, ProductCard, authStore, LoginPage, magentoApi are all typed. The remaining files (ProductListPage, App.jsx, CartPage, ...) follow the same pattern from this and the last three chapters – a good opportunity to apply what you've learned YOURSELF, before chapter 44 continues with automated testing.