---
title: Types
editUrl: https://github.com/Cratis/Screenplay/edit/main/Documentation/screenplay/types.md
description: '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…'
---

A [concept](/screenplay/concepts/) 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

```screenplay
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](/screenplay/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

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](/screenplay/vscode/#optional-values-and-quick-fixes) and [workspace repairs](/screenplay/mcp/authoring-tools/#fix-a-diagnostic) migrate the spelling without changing meaning, comments or alignment. Canonical printing writes `optional` except for [`query Q => observable?`](/screenplay/queries/#observable-queries). That one spelling has no equivalent keyword form, so it stays unchanged, produces no `PLAY0479`, and is excluded from repairs and document migrations.

## Example

```screenplay
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:

```screenplay
event InvoiceRegistered
  invoiceId InvoiceId
  lines     InvoiceLine[]
```

## 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:

```screenplay
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.

## Types compose

A type may reference another type, which is how a nested shape is expressed — by reference rather than by nesting the declaration:

```screenplay
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

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](/screenplay/concepts/#why-a-value-is-personal-data) travel with them where a reader can see them.

## 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:

```text
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?

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