Type Safety
TypeScript inference, type-safe spec building, and structured output schemas.
The catalog system is fully typed end-to-end. TypeScript infers component props and action params directly from your Zod definitions, and utility types let you extract those types for use elsewhere in your codebase.
Type Inference Utilities
Four utility types are exported from @prototyperai/stream-ui/catalog for extracting types from a compiled catalog:
import type {
InferComponentProps,
InferActionParams,
InferCatalogComponents,
InferCatalogActions,
} from "@prototyperai/stream-ui/catalog"InferComponentProps
Extract the inferred props type for a specific component by name. The result is the z.infer of that component's Zod props schema.
type ButtonProps = InferComponentProps<typeof catalog, "Button">
// → { label: string; variant?: "default" | "destructive" | "outline" | "ghost" }
type InputProps = InferComponentProps<typeof catalog, "Input">
// → { label?: string; placeholder?: string; type?: "text" | "email" | "password" }InferActionParams
Extract the inferred params type for a specific action by name:
type SubmitParams = InferActionParams<typeof catalog, "submitForm">
// → { formId: string; validate?: boolean }InferCatalogComponents / InferCatalogActions
Extract the full component or action maps from a catalog. Useful for building generic utilities that operate over all catalog entries:
type Components = InferCatalogComponents<typeof catalog>
// → { Button: ComponentDefinition<...>; Card: ComponentDefinition<...>; ... }
type ComponentNames = keyof InferCatalogComponents<typeof catalog>
// → "Button" | "Card" | "Input" | ...
type Actions = InferCatalogActions<typeof catalog>
// → { submitForm: ActionDefinition<...>; ... }Type-Safe Spec Builder
createSpecBuilder() returns a builder object that constrains element types to components registered in your catalog. If you pass an invalid component name, TypeScript reports an error at compile time.
import { createSpecBuilder } from "@prototyperai/stream-ui/catalog"
const builder = createSpecBuilder(catalog)builder.element()
Create a named UIElement entry. The type parameter is constrained to component names from the catalog:
const btn = builder.element("btn", "Button", { label: "Click me" })
// ✓ "Button" is a valid component name
// @ts-expect-error — "Buttton" is not in the catalog
const bad = builder.element("x", "Buttton", {})The fourth argument accepts optional children, visible, and on bindings:
const card = builder.element("card", "Card", { title: "Welcome" }, {
children: ["btn", "input"],
visible: { $state: "/showCard" },
on: {
press: { action: "submitForm", params: { formId: "main" } },
},
})Prop
Type
builder.spec()
Assemble a complete Spec from named elements:
const spec = builder.spec(
"card",
{
card: card.element,
btn: btn.element,
},
{ showCard: true } // optional initial state
)Prop
Type
Structured Output Schemas
catalog.jsonSchema() returns the full spec as a JSON Schema object, ready for use with any AI provider's structured output feature. catalog.zodSchema() returns the underlying Zod schema.
OpenAI Structured Outputs
const jsonSchema = catalog.jsonSchema()
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: "Build a login form" },
],
response_format: {
type: "json_schema",
json_schema: { name: "ui_spec", schema: jsonSchema },
},
})Anthropic Tool Use
const jsonSchema = catalog.jsonSchema()
const response = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
system: systemPrompt,
messages: [{ role: "user", content: "Build a login form" }],
tools: [{
name: "generate_ui",
description: "Generate a UI spec matching the component catalog",
input_schema: jsonSchema,
}],
})Per-Component Schemas
catalog.componentSchemas() returns individual JSON Schemas for each component and action, useful for documentation or granular validation:
const schemas = catalog.componentSchemas()
// {
// "$schema": "http://json-schema.org/draft-07/schema#",
// "components": {
// "Button": { "type": "object", "properties": { ... } },
// "Card": { ... },
// },
// "actions": {
// "submitForm": { ... },
// },
// }Runtime Validation
After receiving AI output, validate it against the catalog before rendering:
const result = catalog.validate(aiOutput)
if (result.valid) {
// result.data contains the parsed Spec
return <Renderer spec={result.data} registry={registry} />
} else {
console.error("Invalid spec:", result.issues)
}The validate() method performs two-pass validation:
- Structural validation — checks the spec shape, root reference, and component types via the Zod schema
- Per-element prop validation — checks each element's props against its component's Zod schema (expression objects are stripped before validation since they resolve at runtime)
The result includes a valid boolean, the parsed data (when valid), and an issues array with detailed error information:
Prop
Type
Auto-Fixing Invalid Specs
When validation fails, autoFixSpec() can attempt to repair common issues in the spec:
import { autoFixSpec } from "@prototyperai/stream-ui"
const result = catalog.validate(aiOutput)
if (!result.valid) {
const { spec: fixed, fixes } = autoFixSpec(aiOutput)
console.log("Applied fixes:", fixes)
return <Renderer spec={fixed} registry={registry} />
}Expression Schemas
For advanced use cases, the dynamicOf() helper wraps any Zod schema to also accept dynamic expression objects. This is used internally by the catalog to validate props that can be either literal values or runtime expressions.
import { dynamicOf, expressionSchema } from "@prototyperai/stream-ui/catalog"
import { z } from "zod"
// A prop that accepts a literal string OR any expression
const dynamicString = dynamicOf(z.string())
dynamicString.parse("hello") // literal string
dynamicString.parse({ $state: "/user/name" }) // state reference
dynamicString.parse({ $template: "Hi ${/name}" }) // template expression
dynamicString.parse({ $cond: { $state: "/x" }, $then: "a", $else: "b" }) // conditionalThe expressionSchema itself is a union of all expression types:
| Expression | Schema | Description |
|---|---|---|
{ $state: string } | stateExpressionSchema | Read a value from the state model |
{ $item: string } | itemExpressionSchema | Read from the current repeat item |
{ $index: true } | indexExpressionSchema | Current repeat index |
{ $bindState: string } | bindStateExpressionSchema | Two-way state binding |
{ $bindItem: string } | bindItemExpressionSchema | Two-way item binding |
{ $template: string } | templateExpressionSchema | String interpolation |
{ $computed: string } | computedExpressionSchema | Computed value with optional args |
{ $cond, $then, $else } | condExpressionSchema | Conditional value |
All expression schemas are exported individually from @prototyperai/stream-ui/catalog for use in custom validation logic.
Using dynamicOf in Component Definitions
When defining component props that should accept dynamic values, wrap the base schema with dynamicOf():
import { z } from "zod"
import { defineComponent } from "@prototyperai/stream-ui/catalog"
import { dynamicOf } from "@prototyperai/stream-ui/catalog"
export default defineComponent({
description: "A text display component",
props: z.object({
text: dynamicOf(z.string()), // accepts "hello" or { $state: "/msg" }
visible: dynamicOf(z.boolean()).optional(),
}),
})