What built-in types can be constrained in EK9?

← Advanced Type System · Ref: Q720

EK9 allows constraining types that have comparison operators and meaningful value ranges. Not all types qualify.

CONSTRAINABLE BUILT-IN TYPES

These types can be constrained with 'constrain as' or 'constrain':

  Stringconstrain as matches /pattern/ or equality
  Integerconstrain as > 0 and < 100
  Floatconstrain as > 0.0 and < 1.0
  Characterconstrain as >= 'A' and <= 'Z'
  Dateconstrain as >= 2024-01-01 and <= 2024-12-31
  Timeconstrain as >= 09:00 and <= 17:00
  DateTimeconstrain as >= date-time ranges
  Durationconstrain as >= P1D and <= P30D
  Millisecondconstrain as >= 0ms and <= 5000ms
  Dimensionconstrain as >= 0m and <= 100m
  Resolutionconstrain as >= 300dpi
  Moneyconstrain as >= 10000#GBP and <= 120000#GBP
  Colourconstrain as #0099CC or #9900CC
  Versionconstrain as >= 1.0.0-0
  Pathconstrain as equality
  RegExconstrain as equality
  Enumerations — constrain as "Value1" or "Value2"

CONSTRAINABLE USER TYPES

Records and classes can be constrained, subject to TWO requirements.

1. The base must have the operators used in the constraint, taking the literal's type:

  ValidPerson as Person constrain as
    matches /^[a-zA-Z]+ [a-zA-Z]+$/

Fields are NOT addressable inside a constrain block, so a multi-field rule must be folded into ONE operator on the base's public surface.

2. A DEFAULT-CONSTRUCTED base must be UNSET. A constrained type has no unset state of its own - it borrows one from its base - so allocate the fields unset:

  Point
    x as Integer: Integer()   //allocated but UNSET - correct
    y as Integer: 0           //always SET - leaves the constrained type unable to be unset

The synthesised '?' is AND-over-all-fields, so a single set-initialised field makes the whole record set, and 'of(...)' can then Panic or return the base's default in place of a rejected value.

NON-CONSTRAINABLE TYPES

These CANNOT be constrained (E04010 unless stated):

  Boolean — already has only two values
  Bits — has comparison operators but no literal form usable in a constraint
  JSON — dynamic structure, undefined comparison semantics
  Functions — code references, not values
  Traits — cannot be instantiated
  Abstract classes — cannot be instantiated
  Components — singleton services, not value types
  An already-constrained type — constraining is ONE-LEVEL (see Q722)
  Parameterised generics such as 'List of String' (E06132) — a collection is SET when created, so there is no unset state for the constrained type to borrow; and a whole-container predicate like 'contains' is a require condition, not a type invariant (a mutating '+=' would re-validate, so a mutation could break it). Constrain the ELEMENT type instead:
    Tag as String constrain as
      matches /^[a-z]+$/
  then hold 'List of Tag'. That guarantee is stronger - it holds for every element however the list is assembled.

WHY THESE RESTRICTIONS

A constraint compares against a LITERAL, so the base needs both a literal form to compare with and the matching operator. A constrained type also needs an UNSET representation, which it takes from a default-constructed base - a base that is always set cannot supply one.

SIMPLE ALIASING IS ALWAYS ALLOWED

Type aliasing without constraints works for any type:

  Index as Integer          //alias, no constraint
  Name as String            //alias, no constraint

See Q257 for constrained type overview. See Q722 for type hierarchy. See Q721 for constrained types as parameters.

Example

defines module qa.advancedtypes.constrainabletypes

  defines type

    // === STRING CONSTRAINT: regex pattern ===
    Name as String constrain as
      matches /^[a-zA-Z -]+$/

    // === INTEGER CONSTRAINT: range ===
    PositiveIndex as Integer constrain as
      > 0 and < 1000

    // === MONEY CONSTRAINT: salary range ===
    Salary as Money constrain as
      >= 10000#GBP and <= 120000#GBP

    // === DATE CONSTRAINT: year range ===
    RecentDate as Date constrain as
      >= 2020-01-01 and <= 2030-12-31

    // === COLOUR CONSTRAINT: specific values ===
    BrandColour as Colour constrain as
      #0099CC or #9900CC

    // === ELEMENT CONSTRAINT: the right tool for "a list of only these values" ===
    // A parameterised generic cannot be a constrained base (E06132). Constrain the ELEMENT
    // and hold a collection of it - the guarantee then holds for every element.
    Tag as String constrain as
      matches /^[a-z]+$/

    // === SIMPLE ALIAS (no constraint) ===
    Index as Integer

  defines program

    ConstrainableTypesDemo()
      stdout <- Stdout()

      // === STRING CONSTRAINT ===
      // Use the fallible 'of' factory for untrusted input: it returns an UNSET value on a
      // constraint failure rather than Panicking (a bare Name("123!@#") would Panic at runtime,
      // and is a compile error E08260 when the bad value is a literal constant).
      if goodName <- Name().of("Alice Smith")
        stdout.println(`Valid name: ${goodName}`)

      badName <- Name().of("123!@#")
      stdout.println(`Invalid name set: ${badName?}`)

      // === INTEGER CONSTRAINT ===
      if goodIndex <- PositiveIndex().of(42)
        stdout.println(`Valid index: ${goodIndex}`)

      badIndex <- PositiveIndex().of(0)
      stdout.println(`Zero index set: ${badIndex?}`)

      // === MONEY CONSTRAINT ===
      if goodSalary <- Salary().of(50000#GBP)
        stdout.println(`Valid salary: ${goodSalary}`)

      badSalary <- Salary().of(5000#GBP)
      stdout.println(`Low salary set: ${badSalary?}`)

      // === DATE CONSTRAINT ===
      if goodDate <- RecentDate().of(2024-06-15)
        stdout.println(`Valid date: ${goodDate}`)

      badDate <- RecentDate().of(2019-01-01)
      stdout.println(`Old date set: ${badDate?}`)

      // === COLOUR CONSTRAINT ===
      if goodColour <- BrandColour().of(#0099CC)
        stdout.println(`Valid colour: ${goodColour}`)

      badColour <- BrandColour().of(#FF0000)
      stdout.println(`Wrong colour set: ${badColour?}`)

      // === ELEMENT CONSTRAINT RATHER THAN A CONSTRAINED COLLECTION ===
      tags <- List() of Tag
      tags += Tag("alpha")
      stdout.println(`Tags: ${tags}, length ${length tags}`)

      rejectedTag <- Tag().of("NOT-LOWER")
      stdout.println(`Rejected tag set: ${rejectedTag?}`)

      // === SIMPLE ALIAS ALWAYS WORKS ===
      idx <- Index(999)
      stdout.println(`Alias index: ${idx}`)

Common mistakes

E50060 — Constrained types like Name have no getValue() wrapper method, so calling .getValue() fails to resolve. See ek9 -h E50060 for details.

Incorrect:

Name().of("Alice Smith").getValue()

Correct:

Name().of("Alice Smith")

E50060 — Index has no intValue() method in EK9. Use the promote operator (#^) to extract the Integer value. See ek9 -h E50060 for details.

Incorrect:

idx <- Index(999).intValue()

Correct:

idx <- Index(999)
Other ways to ask this
  • Which EK9 types support the constrain keyword?
  • Why can't I constrain a Boolean or JSON type?
  • What is error E04010 about?

Coming from another language?

Java: no built-in constrained types. Bean Validation annotations (@Min, @Max, @Pattern) are runtime-only. Python: no type-level constraints, runtime validation with pydantic or dataclasses. Rust: no built-in constrained types, uses newtype pattern with constructor validation. Ada: subtype constraints on scalar types only (Integer, Float). Go: no type constraints, runtime validation. Kotlin: value classes with init blocks for runtime validation. EK9: built-in constraint syntax for any type with comparison operators — String (regex/equality), Integer/Float (ranges), Date/Time (ranges), Money (ranges), Colour (values), enumerations (value subsets), records/classes (operator expressions).

Keywords: E04010, boolean, unset, date, string, constrainable, built-in, constrain, types, generics, element, integer, list, money, E06132, bits