AMQL Docs
Reusability

Create your own types

Create your own types to give related values a common shape, keep them consistent, and catch mistakes early

A custom type lets you describe a group of related settings once, give each setting a name and a type, and reuse that group across your project.

This guide walks through four ways custom types help with reuse. All of them use the same example: the display settings of a metric (its label, number format, and description).

Give shared settings a shape

Every measure and metric has a label, a format, and often a description. Without a shared definition, each analyst writes them a little differently. A custom type describes the shape once:

Type MetricDisplay {
  label: String
  format (default: '#,###'): String
  description (optional): String
}

MetricDisplay revenue_display {
  label: 'Revenue'
  format: '[$$]#,###'
  description: 'Total order value after discounts'
}

format falls back to '#,###' when it's left out, and description can be skipped. Everything else is required, and AML checks each value against the type. A typo is caught right in the editor:

MetricDisplay revenue_display {
  label: 'Revenue'
  fromat: '[$$]#,###' // error: Property 'fromat' does not exist on type 'MetricDisplay'.
}

Keep values consistent

Free-form strings drift: one analyst writes '[$$]#,###', another '$#,###.00'. To keep everyone on the same few options, list the allowed values in a type and use it for the field:

Type MetricFormat = '#,###' | '[$$]#,###' | '#,###.0%'

Type MetricDisplay {
  label: String
  format (default: '#,###'): MetricFormat
  description (optional): String
}

MetricDisplay refund_rate_display {
  label: 'Refund rate'
  format: '0.00 %' // error: Type 'String' is not assignable to type 'MetricFormat'
}

Adding a new format now means adding it to MetricFormat, which is a single change everyone can review.

Pass one value to a function, not many parameters

When you build measures with a function, take the whole MetricDisplay as one parameter instead of separate label, format, and description parameters:

Func sum_measure(column: String, display: MetricDisplay) => Measure {
  Measure {
    label: display.label
    format: display.format
    description: display.description
    type: 'number'
    definition: @sql {{ #SOURCE.${column} }};;
    aggregation_type: 'sum'
  }
}

Model orders {
  // ...
  measure revenue: sum_measure('amount', revenue_display)
  measure units_sold: sum_measure('quantity', MetricDisplay { label: 'Units sold' })
}

Each call stays short and readable, and adding a new display setting later only changes the type and the function, not every call.

Build bigger settings from smaller ones

Custom types can contain other custom types. For example, you can keep the display settings of a whole area in one catalog, so they're easy to find and review together:

Type MetricCatalog {
  revenue: MetricDisplay
  orders: MetricDisplay
  average_order_value: MetricDisplay
}

MetricCatalog sales_metrics {
  revenue: MetricDisplay { label: 'Revenue', format: '[$$]#,###' }
  orders: MetricDisplay { label: 'Orders' }
  average_order_value: MetricDisplay {
    label: 'Average order value'
    format: '[$$]#,###'
    description: 'Revenue divided by the number of orders'
  }
}

sales_metrics.average_order_value.label // 'Average order value'

Result

  • Shared settings have one agreed shape, and AML checks every value against it.
  • Allowed values are listed in one place, so everyone stays consistent.
  • Functions take one well-named value instead of a long list of parameters.
  • Bigger types are built from smaller ones, so building blocks are reused.

To learn more about declaring types, see AML Types.

On this page