# Types and Schemas CommonTools type system: when to use `Cell<>`, type contexts, and CTS. ## Cell<> = Write Intent `Cell<>` in type signatures indicates **write intent**, not reactivity. Everything is reactive by default. ```typescript // Read-only (still reactive!) interface ReadOnlyInput { count: number; items: Item[]; } // Write access needed interface WritableInput { count: Cell; // Will call .set() items: Cell; // Will call .push() } ``` ### Cell Methods With `Cell` in your signature: | Method | Purpose | |--------|---------| | `.get()` | Read current value | | `.set(value)` | Replace entire value | | `.update({ key: value })` | Partial update (objects) | | `.push(item)` | Add to array | | `.key("property")` | Navigate nested data | Without `Cell<>`, you can still display values in JSX, pass to `computed()`, and map over arrays - all reactively. Note: filtering and transformations must be done in `computed()` outside JSX, then the result can be mapped inside JSX. --- ## Type Contexts Four contexts where types appear differently: ```typescript interface ShoppingItem { title: string; done: Default; } // Context 1: Schema definition interface Input { items: Default; } // Context 2: Pattern parameter (with Cell<> for write access) interface WritableInput { items: Cell; } export default pattern(({ items }) => { // Context 3: items is Cell return { [UI]: (
{/* Context 4: In .map() - item is OpaqueRef */} {items.map((item) => ( {item.title} ))}
), }; }); ``` --- ## Cell vs Cell>> **Use `Cell` by default:** ```typescript const addItem = handler }>( (_, { items }) => { items.push({ title: "New", done: false }); items.set(items.get().filter(x => !x.done)); } ); ``` **Use `Cell>>` only when you need `.equals()` on elements:** ```typescript const removeItem = handler< unknown, { items: Cell>>; item: Cell } >((_, { items, item }) => { const index = items.get().findIndex(el => el.equals(item)); if (index >= 0) items.set(items.get().toSpliced(index, 1)); }); ``` --- ## Handler Types in Output Interfaces Handlers exposed in Output interfaces must be typed as `Stream`, NOT `OpaqueRef`. ```typescript // ✅ CORRECT - Use Stream for handlers in Output interface Output { count: number; increment: Stream; // Handler with no parameters setCount: Stream<{ value: number }>; // Handler with parameters } // ❌ WRONG - OpaqueRef in Output interface interface Output { increment: OpaqueRef; // Wrong! } ``` **Why Stream?** - `Stream` represents a write-only channel for triggering actions - Other charms can call these handlers via `.send()` when linked - `OpaqueRef` is for reactive references in `.map()` contexts, not handlers ### Creating Streams (Bound Handlers) A bound handler IS a `Stream`. Don't try to create streams directly: ```typescript // ❌ WRONG - Stream.of() and .subscribe() don't exist const addItem: Stream<{ title: string }> = Stream.of(); addItem.subscribe(({ title }) => { ... }); // Error! // ✅ CORRECT - Define handler, bind with state const addItemHandler = handler< { title: string }, // Event type { items: Cell } // State type >(({ title }, { items }) => { items.push({ title }); }); // Binding returns Stream<{ title: string }> const addItem = addItemHandler({ items }); // Export in return return { addItem, // This IS Stream<{ title: string }> }; ``` The bound handler is the stream. Other patterns or charms can send events to it via linking. --- ## CTS (CommonTools TypeScript) TypeScript types are automatically processed at runtime. Enable with: ```typescript /// import { pattern, UI, NAME } from "commontools"; ``` CTS provides: - Runtime type validation - Automatic schema generation (for `generateObject`) - Serialization support --- ## Default Specify default values in schemas: ```typescript interface TodoItem { title: string; // Required done: Default; // Defaults to false category: Default; // Defaults to "Other" } interface Input { items: Default; // Defaults to empty array } ``` --- ## Type Patterns **Union types for enums:** ```typescript type Status = "pending" | "active" | "deleted"; // or const StatusValues = ["pending", "active", "deleted"] as const; type Status = typeof StatusValues[number]; ``` **Composition:** ```typescript type Timestamps = { createdAt: Date; updatedAt: Date }; type WithId = { id: string }; type TodoItem = WithId & Timestamps & { title: string; done: boolean }; ``` **Shared types:** ```typescript // schemas.ts export interface TodoItem { title: string; done: Default; } // pattern.tsx import type { TodoItem } from "./schemas.ts"; ``` --- ## Anti-Pattern: Manual OpaqueRef Casting Don't manually cast to/from `OpaqueRef`. The framework handles reactive tracking automatically. ```typescript // ❌ WRONG - Don't cast myHandler({ items: itemsCell as unknown as OpaqueRef }) // ✅ CORRECT - Pass directly myHandler({ items: itemsCell }) // ❌ WRONG - Don't use OpaqueRef in handler types handler[]> }>(...) // ✅ CORRECT - Use plain array type handler }>((_, { items }) => { items.push({ title: "New" }); }) ``` **Why casting breaks things:** - Bypasses reactive tracking - Bypasses TypeScript guidance - Framework already handles this automatically --- ## Summary | Concept | Usage | |---------|-------| | `Cell` | Write access (`.set()`, `.push()`) | | `Default` | Schema default values | | `OpaqueRef` | Auto-wrapped in `.map()` - don't use manually | | `Stream` | Handlers in Output interfaces | | `/// ` | Enable CTS type processing | | `Cell` | Standard array (default) | | `Cell>>` | When you need `.equals()` on elements |