A component that reads PAD's data - the props of an island, the JSON of a page - knows its shape only from what the PHP happens to make. pad types writes that shape down as TypeScript, made from the data itself:

pad types shop orders                         # to standard output
pad types shop orders --out=www/shop/src/types/pad.d.ts

How a value becomes a type:

PHP
fragments/typescript_types_1.php
HTML
fragments/typescript_types_1.pad
Result
<?php

$orders
= [ [ 'id' => 1, 'total' => 9.5, 'customer' => [ 'name' => 'Ada' ] ],
[
'id' => 2, 'total' => 12, 'customer' => [ 'name' => 'Bram' ], 'gift' => TRUE ] ];

$types = padTypeScriptInterface ( 'OrdersVars', [ 'orders' => $orders, 'note' => NULL ] );

?>
{$types}
export interface OrdersVars {
  orders: {
    id: number;
    total: number;
    customer: {
      name: string;
    };
    gift?: boolean;
  }[];
  note: null;
}

A list of rows is one row type - a key some rows lack is optional (gift?), a key whose rows disagree gets a union - and other lists are a union of their items. A component then types its props from the declarations, and tsc --noEmit fails on a field the PHP does not have:

import type { OrdersVars } from './types/pad';

type Order = OrdersVars['orders'][number];

export default function OrderRow(order: Order) { ... }

The islands application types one island this way (examples/typed), its npm run types and npm run check doing both steps.