Types
A concept names a single primitive value. A type names a shape — the structured child records real events routinely carry: the lines of an invoice, the answers of a questionnaire, the contact behind a billing address.
Without it, a document can reference lines InvoiceLine[] but never say what an InvoiceLine is. The reader is left guessing, and the concepts that only ever live inside such a record — a PersonName, an EmailAddress — have no reason to be declared at all, so the document understates the application’s own compliance surface. A type closes both gaps.
Syntax
Section titled “Syntax”type <Name> [description "<text>"] [file <path>] <property> <TypeRef> ...The optional file line names the repository relative file this declaration is realized by, so a document can be navigated back to the code it describes. It is additive - it never stands in for any part of the declaration. See File references.
A type declares at least one property. Events, commands and read models use the same property order: name, type, then optional when the value may be absent. Write a collection as InvoiceLine[] optional when the collection itself may be absent; this does not make its items optional.
optional is case-sensitive and only acts as a modifier after a type. You can still name a property optional (optional String), name a type optional (value optional), or refer to that type optionally (value optional optional). A command identifier must remain required and scalar. Screen data bindings, behavior parameters and concept bases do not accept optionality. Optional reads are reserved for future support; they do not yet describe absence behavior.
Compatibility note
Section titled “Compatibility note”The earlier String? and InvoiceLine[]? spellings remain valid with Information diagnostic PLAY0479; they do not fail --warnaserror. Write String optional and InvoiceLine[] optional in new models. Editor quick fixes and workspace repairs migrate the spelling without changing meaning, comments or alignment. Canonical printing writes optional except for query Q => observable?. That one spelling has no equivalent keyword form, so it stays unchanged, produces no PLAY0479, and is excluded from repairs and document migrations.
Example
Section titled “Example”concept ProductName : Stringconcept Quantity : Intconcept Money : Decimalconcept DiscountPercentage : Decimal
type InvoiceLine description "A single billed line of an invoice" lineNumber Int productName ProductName quantity Quantity unitPrice Money discount DiscountPercentage optionalThe type is then referenced by name from anywhere a type reference is allowed — an event property, a command property, or another type:
event InvoiceRegistered invoiceId InvoiceId lines InvoiceLine[]Structured values in specifications
Section titled “Structured values in specifications”A property declared as InvoiceLine[] accepts an inline list of objects. Quote each key and keep the entire value on one line:
type InvoiceLine sku String quantity IntA specification value for a lines InvoiceLine[] field can then be lines = [{"sku":"A-1","quantity":2}] or lines = []. The compiler checks each key against InvoiceLine, including nested declared types and enum members. A missing declaration or an imported type is not guessed; a key absent from a known shape is an error.
An enum is a concept, not a composite type. Its member names remain the values in specifications and conditions even when every member has an explicit number. The opt-in enum-pinning completeness check also reports its stored-identity hint for an unnumbered enum reached through a composite event property. See Enum concepts for numbering rules and executable admission.
Types compose
Section titled “Types compose”A type may reference another type, which is how a nested shape is expressed — by reference rather than by nesting the declaration:
concept PersonName : String @piiconcept EmailAddress : String @pii
type BillingContact name PersonName email EmailAddress
type Invoice contact BillingContact lines InvoiceLine[]Where types earn their keep — compliance
Section titled “Where types earn their keep — compliance”This is the reason to reach for a type rather than leaving a shape undeclared. A [PII] value buried inside a child record is still personal data, and Chronicle still encrypts and erases it. But if the shape is never declared, nothing in the document says so — and a reader auditing the model sees a smaller PII surface than the application actually has.
Declaring BillingContact gives PersonName and EmailAddress a home in the document, so @pii and its reason travel with them where a reader can see them.
Unresolved references are reported
Section titled “Unresolved references are reported”The compiler resolves every property type reference against the primitives, the declared concepts, the declared types and the imports. A reference to something the document never declares is a warning, not an error — a runtime may well resolve the name, but a document that silently depends on a shape living outside it is exactly what the warning is there to surface:
Unknown type 'InvoiceLine' on 'lines' of event 'InvoiceRegistered' -declare it with 'concept InvoiceLine : <Primitive>' or 'type InvoiceLine'Concept and type names share one namespace — declaring both a concept InvoiceLine and a type InvoiceLine is an error.
Type or concept?
Section titled “Type or concept?”| Use | When |
|---|---|
concept | The value is a primitive with a domain meaning — an id, a name, an amount, a code. |
type | The value is a shape made of several such values. |
Reach for a concept first. A type exists for the shapes a single primitive cannot express — and its properties should themselves be concepts, so the strong typing goes all the way down.