Building a type from other types
Most object types in a real application are slices of an entity or of another object: a create input, a patch input where every field is optional, a public view, a list summary, a v2 response that…
Most object types in a real application are slices of an entity or of another
object: a create input, a patch input where every field is optional, a public
view, a list summary, a v2 response that adds a field. Instead of writing
each field out (title: Todo.title), a type's body can copy fields from
other types. The body keeps its ordinary Output { } form. Four kinds of
line build it. They are read from top to bottom, and each changes the
result of the lines above it:
| Line | Does |
|---|---|
include: [Todo] |
adds every field of each listed type (types only; never on an Input) |
pick: [Todo.id, Todo.title] |
adds just the listed fields, each keeping its own name |
omit: [Todo.internal_note] |
removes fields present at that point |
bar: User.bar |
a written field: replaces a field of the same name above, or adds a new one |
type TodoView = Output { pick: [Todo.id, Todo.title, Todo.status, Todo.due_at]}type TodoDetail = Output { include: [Todo] omit: [Todo.deleted_at] owner_email: User.email}type PatchTodo = Input { pick: [Todo.title, Todo.due_at] optional: all}type OrderView = Output { include: [Order] lines: List<OrderLineView> // replaces Order.lines with a view of each line}type TodoReminder = Object { pick: [Todo.id, Todo.title, Todo.due_at] owner_email: User.email days_overdue: DayCount}
Rules:
- Top to bottom. The same kind of line may appear more than once, and the formatter keeps the order written, because order matters here.
- Fields keep their declared order. Copied fields stand where their
includeorpickline is, aninclude's in the order its type declares them and apick's in the order listed; a written field that replaces a copied one takes its place. Values, hover and the manifest (each field'sorder) follow it. - Lists are always in brackets, even with one entry:
include: [Todo]. omitremoves only what is there. Omitting a field that is not present at that point is an error, which catches typos and fields since removed.- Written fields win. A field you write replaces any field of the same name above it, visibly; otherwise it is added.
- Copied fields never silently replace copied fields. An
includeorpickthat would replace a field brought in by an earlierincludeorpickis an error: resolve it withomitor by writing the field. Copied fields are not visible in the declaration, so a clash there would change a type unseen when a source type gains a field. - No
includeon anInput. Taking every field ofUserwould accept any field added toUserlater, so a client could set a field it was never meant to (the mass-assignment bug). Inputspickor write their fields. Generated fields such asidandcreated_atcan never appear in anInput. optionalmakes the listed fields, orall, optional, only on anInput.- Copied fields follow their source. Renaming
Todo.titlerenames it in every type that copies it, and its validation rules come with it. - No type includes itself. An
includethat leads back to its own type, directly or through another, istype.copy_cycle. - Every built type has a name; there are no inline types in signatures.
Hover in your editor shows the final fields and where each came from
(
title ← Todo.title,lines ← written, replaces Order.lines). - Outputs may
include. A field added toTodolater reachesTodoDetail, and the committed API record shows the addition in review (HTTP). Fields that policy hides from clients stay hidden when the app runs (Field restrictions). include,pick,omitandoptionalare reserved and cannot be field names.
Markdown for agents: /docs/building-a-type-from-other-types.md.Built from Jadpo main at a707efb.