# AML Custom Types > Define your own AML types to group related values, give every field a name, and let AML catch typos and missing values for you ## Introduction Besides the [built-in types](/reference/aml/types) like `String` and `Number`, AML lets you define your own types. A custom type (also called a record type) groups a few related values under one name, and gives each value a field name and a type. To see why that helps, imagine a [function](/reference/aml/func) that builds a KPI card for your dashboards: ```aml Func kpi_card(title: String, metric: FieldRef, format: String, color: String, show_trend: Boolean) { VizBlock { label: title // ... } } Dashboard sales { block revenue: kpi_card('Revenue', r(orders.total_revenue), '$#,###', '#2E7D32', true) block orders: kpi_card('Orders', r(orders.count), '#,###', '#1565C0', false) } ``` It works, but each call is a row of unnamed values. It's hard to tell which string is the format and which is the color, it's easy to swap two of them by accident, and every call has to repeat all five values. With a custom type, you describe the settings once and pass them around as one value: ```aml Type KpiSettings { title: String metric: FieldRef format (default: '#,###'): String color (optional): String show_trend (default: false): Boolean } Func kpi_card(settings: KpiSettings) { VizBlock { label: settings.title // ... } } Dashboard sales { block revenue: kpi_card(KpiSettings { title: 'Revenue' metric: r(orders.total_revenue) format: '$#,###' show_trend: true }) block orders: kpi_card(KpiSettings { title: 'Orders' metric: r(orders.count) }) } ``` Every value now has a label, fields you skip fall back to their defaults, and AML underlines a misspelled or missing field right in the editor. ## Syntax Declare a custom type with the `Type` keyword, followed by its name and a list of fields: ```aml Type { : (optional): (default: ): } ``` * By convention, type names use `PascalCase` (e.g. `KpiSettings`), and field names use `snake_case`. * A field can use any type: a built-in type, a literal like `'asc'`, a [union](#limit-a-field-to-a-few-values) of values, or another custom type. * A field is **required** unless you mark it `(optional)` or give it a `(default: ...)` value. These are two of AML's [field rules](#advanced-field-rules), which also let a field accept repeated blocks. Once declared, the type name works in two ways. You can use it to declare a named value, or call it like a constructor to create a value inline: ```aml // Declare a named value KpiSettings revenue_settings { title: 'Revenue' metric: r(orders.total_revenue) } // Create a value inline, for example as a function argument kpi_card(KpiSettings { title: 'Orders', metric: r(orders.count) }) ``` To read a field, use a dot: `revenue_settings.title` returns `'Revenue'`. ## Example usages ### Optional fields and default values A field marked `(optional)` can be left out. A field with `(default: ...)` can also be left out, and AML fills in the default for you. ```aml Type KpiSettings { title: String format (default: '#,###'): String color (optional): String } KpiSettings orders_settings { title: 'Orders' } orders_settings.format // '#,###' (filled in from the default) ``` :::tip If your code reads a field, prefer a default value over `(optional)`. An optional field that was left out has no value, so reading it gives you nothing to work with. A default guarantees the field always has something sensible. ::: ### Limit a field to a few values Sometimes a field should only accept a few specific values. List them with `|` (read it as "or"): ```aml Type SortSetting { field: String direction: 'asc' | 'desc' } SortSetting by_revenue { field: 'revenue' direction: 'up' // error: Type 'String' is not assignable to type '"asc" | "desc"' } ``` If you use the same set of values in several places, give it a name with `Type = ...`. This creates a type alias, a shorter name for an existing type: ```aml Type SortDirection = 'asc' | 'desc' Type SortSetting { field: String direction (default: 'asc'): SortDirection } Type TableSettings { default_sort_direction: SortDirection // ... } ``` You've already seen aliases on the [AML Types](/reference/aml/types) page: `Type ListInt = List[Int]` works the same way. ### Nested types A field's type can be another custom type. This is handy when a group of fields belongs together and you want to reuse it: ```aml Type Threshold { value: Number color: String } Type KpiSettings { title: String warning: Threshold critical: Threshold } KpiSettings churn_settings { title: 'Churn rate' warning: Threshold { value: 0.05, color: '#F59E0B' } critical: Threshold { value: 0.1, color: '#DC2626' } } churn_settings.critical.color // '#DC2626' ``` If a group of fields is only used in one place, you can write it inline instead of declaring a separate type: ```aml Type KpiSettings { title: String layout { // an inline type (`layout: {` also works here) width: Number height (default: 200): Number } } KpiSettings revenue_settings { title: 'Revenue' layout { // no `:` here: `layout: {` would create a Dict instead width: 400 } } revenue_settings.layout.height // 200 ``` ### Use custom types with functions Custom types are most useful as function parameters, as shown in the [introduction](#introduction). They also work as return types and as list elements: ```aml Type KpiSettings { title: String metric: FieldRef } // Return a custom type Func revenue_settings(region: String) => KpiSettings { KpiSettings { title: 'Revenue in ${region}' metric: r(orders.total_revenue) } } // Accept a list of custom-type values Func first_title(items: List[KpiSettings]) => String { items(0).title } ``` ## Advanced: field rules `(optional)` and `(default: ...)` are field rules: options in parentheses after a field name that change how the field behaves. AML has several more, and they let a field accept **repeated blocks**. That's how a chart accepts any number of `calculation calc { ... }` blocks, or a model any number of `dimension id { ... }` blocks. Holistics uses these rules to define its own built-in types, and you can use them in yours. Most custom types don't need them. Reach for them when you want your type to read like a built-in one. | Rule | What it does | Used with | | --- | --- | --- | | `optional` | The field can be left out | Any field | | `default: ` | The field can be left out, and gets this value | Any field | | `multiple` | The field can appear many times. Blocks are stored as a list | | | `elem_name_as: ''` | Each block has a name (`threshold warning { }`), stored under `` | Usually `multiple` | | `unique_elem_name` | Names must be unique. Blocks are stored by name | `elem_name_as`, `multiple` | | `root_namespace` | Named blocks can also be read directly from the parent value | `elem_name_as`, `unique_elem_name` | | `allow_no_body` | Blocks can leave out `{ }` (`model orders`) | `elem_name_as` | | `elem_type_as: ''` | Blocks start with your own keyword (`not_null user_id { }`), stored under `` | One field per type | | `unique_elem_type` | Each keyword can be used once. Blocks are stored by keyword | `elem_type_as` | Separate rules with commas, like `(elem_name_as: 'name', multiple, unique_elem_name)`. A rule without a value, like `multiple`, is short for `multiple: true`. A field that accepts repeated blocks is always optional: a value can leave it out entirely. :::note The names and keywords these rules store (`name` and `kind` in the examples below) are part of the value, but AML code can't read them yet. For example, `churn_settings.threshold.warning.name` is an error. Use the name or keyword to look up a block instead. The exception is [`elem_name_as` on a type](#name-the-value-itself-elem_name_as-on-a-type), which you can read. ::: ### Repeated blocks: `multiple` With `multiple`, a field can appear as many times as you need. The blocks are stored as a list, in the order you wrote them: ```aml Type KpiSettings { title: String comparison (multiple) { label: String offset: Int } } KpiSettings revenue_settings { title: 'Revenue' comparison { label: 'vs last month', offset: -1 } comparison { label: 'vs last year', offset: -12 } } revenue_settings.comparison(1).label // 'vs last year' ``` ### Named blocks: `elem_name_as` and `unique_elem_name` Lists are fine until you need a specific block. To find blocks by name, give each one a name right after the field name. `elem_name_as` tells AML which key to store the name under, and `unique_elem_name` stores the blocks by that name: ```aml Type KpiSettings { title: String threshold (elem_name_as: 'name', multiple, unique_elem_name) { value: Number color: String } } KpiSettings churn_settings { title: 'Churn rate' threshold warning { value: 0.05, color: '#F59E0B' } threshold critical { value: 0.1, color: '#DC2626' } } churn_settings.threshold.critical.color // '#DC2626' ``` With `unique_elem_name`, using the same name twice is an error (`Duplicated name 'warning'.`). Without it, names can repeat, and the blocks are stored as a list like with `multiple` alone (`churn_settings.threshold(0)`). ### Read named blocks directly: `root_namespace` Add `root_namespace` to skip the field name when reading a named block. Both forms below work: ```aml Type KpiSettings { title: String threshold (elem_name_as: 'name', multiple, unique_elem_name, root_namespace) { value: Number color: String } } KpiSettings churn_settings { title: 'Churn rate' threshold critical { value: 0.1, color: '#DC2626' } } churn_settings.critical.color // '#DC2626' churn_settings.threshold.critical.color // '#DC2626' ``` `root_namespace` needs `unique_elem_name`, since names have to be unique to be looked up. ### Blocks without a body: `allow_no_body` Sometimes the name is all you need, like listing models with `model orders`. `allow_no_body` lets a named block leave out its `{ }`: ```aml Type Report { model (elem_name_as: 'name', multiple, unique_elem_name, allow_no_body) { label (optional): String } } Report sales_report { model orders // no body needed model users { label: 'Customers' } } sales_report.model.users.label // 'Customers' ``` If the blocks never need a body, leave `{ }` out of the type as well: ```aml Type ReportSettings { title: String hidden_field (elem_name_as: 'name', multiple, unique_elem_name, allow_no_body) } ReportSettings sales_report { title: 'Sales' hidden_field customer_email hidden_field phone_number } ``` ### Your own keywords: `elem_type_as` and `unique_elem_type` `elem_type_as` lets blocks start with a keyword you choose, instead of the field name. In `not_null user_id { }` below, `not_null` is the keyword (stored under `kind`) and `user_id` is the name (stored under `column`): ```aml Type ColumnChecks { table: String check (elem_type_as: 'kind', elem_name_as: 'column', unique_elem_name) { severity (default: 'error'): String } } ColumnChecks orders_checks { table: 'orders' not_null user_id {} unique order_id { severity: 'warn' } } orders_checks.check.order_id.severity // 'warn' ``` Any line that doesn't match another field (like `table`) is checked against the `elem_type_as` field, so the keyword can be any word. For the same reason, only one field per type can use `elem_type_as`. With `unique_elem_type`, blocks are stored by keyword instead, and each keyword can be used only once: ```aml Type ColumnChecks { check (elem_type_as: 'kind', unique_elem_type) { columns: List[String] } } ColumnChecks orders_checks { not_null { columns: ['user_id', 'order_id'] } unique { columns: ['order_id'] } } orders_checks.check.not_null.columns(1) // 'order_id' ``` ### Name the value itself: `elem_name_as` on a type `elem_name_as` also works on the type itself. Then a value of that type stores the name it's declared with, and unlike the field rules above, you can read it: ```aml Type KpiSettings (elem_name_as: 'id') { title: String } KpiSettings revenue_kpi { title: 'Revenue' } revenue_kpi.id // 'revenue_kpi' ``` Built-in chart types use this to give each chart a name. `elem_name_as` is the only rule allowed on a type. ## See also - [AML Types](/reference/aml/types) - [AML Function](/reference/aml/func) - [Build a Dashboard with Multiple Similar Charts](/docs/canvas-dashboard/build-similar-dashboards)