Skip to content
HomePagesHomePages template kit

props-from-schema

"A Renderer's Props type must be derived from schema.ts, not hand-written."

Venue: eslint — A Renderer’s Props type must be derived from schema.ts, not hand-written.

The rule runs only on Renderer.tsx. It reports an exported type alias or interface whose name ends in PropsProps, 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 beside the schema declaration; 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 "./section.js"; is the shape the rule is steering you to, and it reports nothing.

The schema is the section’s single source of truth, and the prop type is inferred from itSectionProps<typeof schema> reads the schema literal and resolves each slot to its runtime value shape: a text becomes string (blank is ""), an optional number becomes number | null, an image becomes ImageValue, and so on.

A hand-written Props shadows that inference, and then drifts silently. Add a slot to the schema 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 beside the schema, in section.ts — and import it in the Renderer:

export type Props = SectionProps<typeof schema>;
import type { Props } from "./section.js";
sections/hero/Renderer.tsx
import { Section } from "@homepages/template-kit";
export type Props = {
slots: { headline: string };
};
export function Renderer({ slots }: Props) {
return (
<Section>
<h1 data-slot-id="headline" data-slot-text-leaf="">{slots.headline}</h1>
</Section>
);
}
sections/hero/section.ts
import { defineSchema, type SectionProps, text } from "@homepages/template-kit";
export const schema = defineSchema({
label: "Hero",
slots: {
headline: text.medium({ label: "Headline" }),
},
});
export type Props = SectionProps<typeof schema>;
sections/hero/Renderer.tsx
return (
<Section>
<Slot.Text slot={slot.headline} as="h1" />
</Section>
);

The builders preserve the literal types SectionProps needs: without them the types are widened away and SectionProps can no longer tell one slot kind from another.

  • Renderer.tsxSectionProps inference off the schema literal, and why the prop type is never hand-written.