The contract is generated: how PHP becomes TypeScript in Lattice
Part 11 of the Building Lattice series. Part 10 covered component packages.
Back in Part 2 I made a claim I never fully backed up: that drift between the PHP side and the TypeScript side of Lattice is a compile error, not a runtime surprise. This post is the machinery behind that sentence. The short version: nobody writes the TypeScript types. They’re generated from the PHP classes — all of them, including the framework’s own.
One command
php artisan lattice:typescript
In an app, that scans your discover roots and writes
resources/js/lattice/generated.d.ts — a module augmentation that merges
your custom components’ prop types into @lattice-php/lattice. If you have no
custom wire types, it tells you the bundled types already cover you and writes
nothing. Inside the Lattice repo itself, the same command regenerates the
built-in generated.ts that ships in the npm package. There is no second code
path: the framework eats exactly what it serves.
A class crosses over
Four attributes drive generation. #[TypeScript] marks a plain enum or value
object for export. #[WireType] is the interesting one — it carries the type
string that discriminates a node on the wire, and every family root extends it:
#[AsComponent], #[AsColumn], #[AsFilter], #[AsEffect]. So the attribute
you’ve seen on every class in this series is the type generator’s input.
#[WireEnvelope] names the envelope an abstract marker generates as (Node,
ColumnNode, FilterNode), and #[WireMap] forces an array prop to stay a
JSON object, arriving as a Record instead of an accidental array.
From there it’s public properties in, types out:
#[AsComponent('heading')]
class Heading extends Component
{
public string $text = '';
public int $level = 1;
}
export type Heading = {
copyable: boolean;
level: number;
text: string;
tooltip: string | null;
};
The extra keys come from traits (HasCopyable, HasTooltip) — the generator
sees the serialized surface, not your source file. Every prop is required;
nullable PHP becomes T | null.
Who wins: your types, the built-ins, or the fallback
The generated module resolves a node’s props with one small calculus:
export type ResolveProps<TAugment, TBuiltins, TType extends string, TFallback>
= TType extends keyof TAugment
? TAugment[TType]
: TType extends keyof TBuiltins
? TBuiltins[TType]
: TFallback;
Your app’s augmentation wins, the built-in map is next, and an unknown type degrades to a loose props bag instead of an error. That ordering is what makes Part 10’s packages work: a package’s components get real prop types the moment you run the generator, and a renderer for a type nobody generated still compiles.
The last hand-written payload
Until 0.21 there was one place the contract was still maintained by hand: the
top-level page payload. That’s gone now — PagePayload is generated from the
PHP class like everything else, and with it the per-domain node unions and even
subclass relationships. A prop typed as Action in PHP generates as
Node<"action"> | Node<"action.bulk"> | null, because the generator knows
BulkAction is a concrete descendant. The type system on the client now knows
things about the class hierarchy on the server.
Trust, but verify in CI
Generated code you can ignore is generated code that rots. The committed
generated.ts is guarded twice: a snapshot test regenerates it and asserts
byte-equality, and CI runs the generator and fails on git diff --exit-code.
Change a public property on a PHP component and forget to regenerate, and the
build goes red before any frontend code ever runs against the stale shape.
Generation is deterministic across PHP versions, so the diff is meaningful.
The full story, including hand-augmenting types for edge cases, is in the
registry and types docs.
That’s the compile-time half of trusting the contract: the shapes can’t drift. But types prove what a payload looks like, not what your code does — a form that validates the wrong field has a perfectly well-typed wire format. Next: the testing story, where the schema tree itself becomes the assertion target.