Prototyper UI

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:

  1. Structural validation — checks the spec shape, root reference, and component types via the Zod schema
  2. 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" }) // conditional

The expressionSchema itself is a union of all expression types:

ExpressionSchemaDescription
{ $state: string }stateExpressionSchemaRead a value from the state model
{ $item: string }itemExpressionSchemaRead from the current repeat item
{ $index: true }indexExpressionSchemaCurrent repeat index
{ $bindState: string }bindStateExpressionSchemaTwo-way state binding
{ $bindItem: string }bindItemExpressionSchemaTwo-way item binding
{ $template: string }templateExpressionSchemaString interpolation
{ $computed: string }computedExpressionSchemaComputed value with optional args
{ $cond, $then, $else }condExpressionSchemaConditional 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(),
  }),
})

On this page