Surface / foldkit-surface

Field references

Surface.application generates a reference tree from the Model Schema, one node per field. Each FieldRef carries its optic, codec, get, and set, plus the field name as a literal type, so a selection can infer its own output keys without a parallel field registry.

ts
App.model.todos                 // FieldRef<Model, Todo[], 'todos'>
App.model.selectedTodoId        // FieldRef<Model, Option<string>, 'selectedTodoId'>
App.model.todosById.at('t1')    // OptionalRef<Model, Option<Todo>> (dynamic key)
App.model.todos.index(0)        // OptionalRef<Model, Option<Todo>> (dynamic index)

A Model field that is itself a Schema.Struct recurses, so each of its fields is a FieldRef too. Array and record access is .index(i) / .at(key), which returns an OptionalRef — a ModelRef with an Option value. Those are dynamic selections, so Projection.pick (which needs a static field name) rejects them; they are useful inside a Projection.

App.fields is the same tree under a second name, kept only so older code compiles. It is deprecated; use App.model.

Projection.pick turns references into a writable projection — a Schema.Struct, get, and set:

ts
const Shared = Projection.pick(App.model.todos, App.model.selectedTodoId)
// { schema, dependencies, get, set }

Each picked field is named by its last key, so App.model.post.id is id. Two fields with the same name at different paths, such as post.id and viewer.id, would be one field, and are refused when the projection is built; pick them in separate projections. Projection.compose refuses the same collision between its parts.

A reference from a different application is rejected by a per-application owner token, so two structurally identical Models cannot be mixed. Duplicate members deduplicate; a conflicting definition throws.

Projection.compose merges disjoint writable projections, keeping each part's own write behavior. ModelRef.fromOptic builds a reference from an optic you already have.