Skip to content

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.

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.

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.

concept ProductName : String
concept Quantity : Int
concept Money : Decimal
concept DiscountPercentage : Decimal
type InvoiceLine
description "A single billed line of an invoice"
lineNumber Int
productName ProductName
quantity Quantity
unitPrice Money
discount DiscountPercentage optional

The 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[]

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 Int

A 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.

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 @pii
concept 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.

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.

UseWhen
conceptThe value is a primitive with a domain meaning — an id, a name, an amount, a code.
typeThe 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.