template-kit/props-from-schema
The template-kit/props-from-schema authoring rule — what it enforces, why, and how to fix it.
A Renderer’s props type is derived from
schema.ts, never hand-written in the Renderer.
The rule runs only on Renderer.tsx. It reports an exported type alias or interface
whose name ends in Props — Props, HeroProps, GalleryProps.
Two consequences of that being the whole test:
- It does not read the right-hand side. Even
export type Props = SectionProps<typeof schema>;reports if you write it in the Renderer. The derivation belongs inschema.ts; the Renderer imports it. - A non-exported local type is invisible to the rule. An internal
type Row = { id: string };used to type a helper’s parameter is your business, and is not reported.
import type { Props } from "./schema.js"; is the shape the rule is steering you to, and
it reports nothing.
Reason
Section titled “Reason”The schema is the section’s single source of truth, and the prop type is inferred from
it — SectionProps<typeof schema> reads the schema literal and resolves each slot to
its runtime value shape: a required text becomes string, an optional one
string | null, an image becomes ImageValue, and so on.
A hand-written Props shadows that inference, and then drifts silently. Add a slot
to schema.ts and forget to render it: with the derived type, reading slots.subhead in
the Renderer is a compile error until the slot exists, and the platform’s tooling and
your renderer agree by construction. With a hand-written Props, the Renderer keeps
compiling happily against the old shape — while the editor, the fill pipeline, and the
schema all now believe in a slot your markup has never heard of. Nothing fails until a
user is looking at a page with a missing block in it.
It also forces the editor to reason about a second definition of the same thing.
Declare the derived type once in schema.ts and import it in the Renderer:
export type Props = SectionProps<typeof schema>;import type { Props } from "./schema.js";Before
Section titled “Before”import { Section, Slot } from "@homepages/template-kit";
export type Props = { slots: { headline: string };};
export function Renderer({ slots }: Props) { return ( <Section> <Slot id="headline" as="h1" textLeaf> <span>{slots.headline}</span> </Slot> </Section> );}import type { SectionProps, SectionSchemaShape } from "@homepages/template-kit";
export const schema = { meta: { displayName: "Hero" }, slots: { headline: { type: "text", size: "medium", required: true, produced_by: "headline_text" }, },} as const satisfies SectionSchemaShape;
export type Props = SectionProps<typeof schema>;import { Section, Slot } from "@homepages/template-kit";
import type { Props } from "./schema.js";
export function Renderer({ slots }: Props) { return ( <Section> <Slot id="headline" as="h1" textLeaf> <span>{slots.headline}</span> </Slot> </Section> );}The as const satisfies SectionSchemaShape is load-bearing: without it the literal types
are widened away and SectionProps can no longer tell a required slot from an optional
one.
See also
Section titled “See also”- Schema system — the schema literal,
SectionPropsinference, and the four contract files.