---
title: "Component Contracts and Schemas"
description: "component(name, contract, fn) declares props, defaults, events and api in one literal, then derives coercion, a generated .d.ts and a conformance proof."
source: https://lemonadejs.com/docs/contracts/
---

# Contracts and component schemas

A plain function is a component. `component(name, contract, fn)` is what
you do when a component becomes a **product**: a block, a library entry,
anything other frameworks or other agents consume. The contract declares
the interface once, as a *runtime artifact*, and everything derives from
it: coercion, generated types, and a machine-readable schema. This is the real Switch block, abridged:

```javascript
import { component, html } from 'lemonadejs';

export const Switch = component('switch', {
    bind: Boolean,                // bindable value, type inferred
    checked: false,               // prop: boolean, default false
    label: '',                    // prop: string, default ''
    size: '',                     // attribute "small" arrives coerced, typed
    disabled: false,
    onchange: Function,           // event out
    api: { toggle: Function },    // imperative surface via props.ref
}, (props, { bind }) => {
    const current = bind(props, props.checked.value);
    const toggle = () => {
        if (!props.disabled.value) current.set(!current.value);
    };
    props.ref?.({ toggle });      // fulfill the declared api
    return html`<label class="lm-switch ${() => current.value ? 'lm-switch-on' : 'lm-switch-off'}">
        ...
    </label>`;
});
```

One literal, four kinds of entry: **literal values** declare a prop with
its type *and* default (`label: ''` → string, default `''`),
**constructors** declare type only (`data: Array`), **`on*` keys** declare
events out, **`api: {...}`** declares the imperative surface delivered
through `props.ref`. The optional **`bind`** entry declares the two-way
value, so its type and default are inferred the same way, and a contract
default feeds the [bind() tool](/docs/two-way-binding/) when the caller
leaves the component unbound.

The contract is the declaration: props typed and defaulted, `bind`, an event and an api. `contract()` reads it back.

<!--example-->

```js
import { html, component, contract } from 'lemonadejs';

const Switch = component('lightswitch', {
    bind: Boolean,
    label: '',
    disabled: false,
    onchange: Function,
    api: { toggle: Function },
}, (props, { bind }) => {
    const on = bind(props, false);
    const toggle = () => !props.disabled.value && on.set(!on.value);
    props.ref?.({ toggle });
    return html`<button onclick="${toggle}"
        style="${() => 'padding:6px 14px;border-radius:16px;border:1px solid #999;background:' + (on.value ? '#ffd43b' : '#eee')}">
        ${props.label}: ${() => (on.value ? 'on' : 'off')}
    </button>`;
});

const App = (props, { state }) => {
    const dark = state(false);
    const changes = state(0);
    return html`<div>
        <${Switch} bind="${dark}" label="Dark mode" onchange="${() => changes.value++}" />
        <p>bound value: <b>${dark}</b> · onchange fired ${changes} times</p>
        <pre>${JSON.stringify(contract(Switch), null, 2)}</pre>
    </div>`;
};
```

## What derives from the contract

**Declared props arrive as live states, in every deployment.** This is
the key rule of the contract layer. Whether the component is mounted by a
lemonade parent, driven by a custom element attribute, or wrapped by the
React adapter, `props.label` is a `State<string>`: `${props.label}` in
templates, `props.label.value` in logic. One component model everywhere.
The wrapper normalizes whatever the caller sent: a live state is shared
as-is (the caller keeps write access), a plain value is boxed, a missing
prop becomes a state holding the default.

**And they are non-optional, precisely typed.** Because the engine
constructs every declared prop, the types say so: `props.max` is
`State<number>`, not `State<number> | undefined`, so
`props.max.value - props.min.value` typechecks with no `!` and no `as`
casts anywhere in the component body. Declared `on*` events are
invocable as-is (`props.onchange?.(value)`), and the api type a ref
receives is named by `ApiOf<typeof Switch>`: never hand-roll an api
shape the contract already defines.

**Attribute strings are coerced to the declared type.** `"5"` becomes
`5` for a number prop; booleans follow HTML semantics, so present (even
empty) is true, `"false"` and `"0"` opt out; a removed attribute returns
a boolean to `false` and anything else to its default. Declared coercion
is honoring a contract, not guessing. Passing a genuinely wrong type
warns `LJS-401` in dev, and contract prop names must be lowercase, because they
become HTML attributes (`LJS-401` warns), as must event names
(`LJS-305`). A prop the contract does **not** declare warns `LJS-402`
with a did-you-mean suggestion (edit distance against the declared names
against the declared names, so `lable` suggests `label`); `ref`, `children`, `expose` and declared
`on*` events are always accepted and never warn.

**Two contract subtleties worth declaring deliberately.** `null` means
*any*: use it for union-typed values a constructor cannot express, so
`bind: null` for a `string | number` id leaves the bindable value
unchecked. And declaring your own `onchange` alongside `bind` keeps
**your** signature: `onchange(id, node)` is fine; bind's default
`(value, oldValue)` shape applies only when you do not declare one.

**`contract(C)` returns the schema at runtime.** Plain JSON (name,
props with type and default, bind, events, api): the interface an agent
reads *instead of the source*, in one request:

```javascript
import { contract } from 'lemonadejs';
contract(Switch)
// { name: 'switch',
//   props: { checked: { type: 'boolean', default: false },
//            label:   { type: 'string',  default: '' }, ... },
//   bind: { type: 'boolean' }, events: ['onchange'], api: ['toggle'] }
```

**The `.d.ts` is a projection, never a source.** The Studio build
generates each block's type declarations *from* the contract, and the file
opens with `GENERATED from contract.json, do not edit`. TypeScript
humans get editor types, agents get JSON, and they cannot disagree
because both are projections of the same literal.

**`verify(C)` is the conformance proof.** From `lemonadejs/test`, it
exercises the contract mechanically: mounts with defaults, mounts with
every prop as a plain value *and* as a live state (then touches it),
passes every declared event a callback, drives `bind` with external
writes, and asserts every declared api method is actually exposed through
`props.ref`. The strict part: **any `LJS-*` engine warning during any
check fails that check**. Conformance includes silence: a component
that mounts but warns does not pass.

```javascript
import { verify } from 'lemonadejs/test';
verify(Switch)   // { component: 'switch', pass: true, checks: [...] }
```

## In practice

The Studio registry is the receipt. `npm run registry` in the library
repo extracts every block's contract, runs `verify()` against it in
jsdom, generates the `.d.ts` projection, and writes four artifacts per
block: `contract.json`, `verify.json`, `dist/index.d.ts`, and an entry in
`components/registry.json`, the search index an agent reads in one
request. The script is a **gate**: any block whose proof fails breaks the
build (`process.exit(1)`). No contract, no entry; no passing proof, no
entry.

Current state of the registry: **43 blocks, all verified, 786 checks**.
The Switch above passes 22: one mount, nine props twice each (plain and
live), the event, the bind, the api. The datagrid's contract declares 8
props, 5 events and a 6-method api (`getSelected`, `setSearch`, `sort`,
`page`, `refresh`, `setColumn`), and an agent choosing whether to use it
reads that from `contract.json` without ever opening the 600-line
source.

Publishing is the *only* step that changes. App-internal components stay
plain functions: the four [template rules](/docs/templates/), nothing
else. The contract is one extra wrapper for a different act: shipping.
And once published, the same literal drives every
[deployment](/docs/deployments/): `createWebComponent(Switch)` derives a
full custom element, `adaptReact(Switch)` a first-class
[React component](/docs/react/), and `<${Switch} expose />` publishes the
declared api as an app-wide service (see [Sugar](/docs/sugar/)).

## Reference

```javascript
import { component, contract } from 'lemonadejs';

const C = component(name, contractLiteral, fn);
// contractLiteral entries:
//   prop: literalValue        type + default inferred (string/number/boolean/array/object)
//   prop: Constructor         type only (String, Number, Boolean, Array, Object, Function)
//   prop: null                ANY type (unions: bind: null for string|number ids)
//   onsomething: Function     declared event (lowercase, LJS-305); declaring
//                             onchange beside bind keeps YOUR signature
//   bind: value|Constructor   bindable value; default feeds bind() when unbound
//   api: { method: Function } imperative surface via props.ref, ApiOf<typeof C>
//                             names the type a ref receives

contract(C)        // → { name, props: { k: { type, default? } }, bind, events, api } | null

import { verify } from 'lemonadejs/test';
verify(C)          // → { component, pass, checks: [{ name, pass, detail? }] }
                   //   any LJS-* dev warning during a check fails it
```

Coercion (attribute strings → declared type): `"5"` → `5` for numbers;
booleans use presence semantics (`""` is true, `"false"`/`"0"` are
false); removed attribute → `false` for booleans, the default otherwise.
Inside the component, declared props are always states, **non-optional**
(`ContractProps<C>`; no `!`, no casts); callers may pass states, plain
values or attribute strings (`ContractInput<C>`). Wrong types and
non-lowercase names warn `LJS-401`; undeclared props warn `LJS-402` with
a did-you-mean hint; see [Errors](/docs/errors/) for the full code table.