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
  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
  Moneyconstrain as >= 10000#GBP and <= 120000#GBP
  Colourconstrain as #0099CC or #9900CC
  Enumerations — constrain as "Value1" or "Value2"

CONSTRAINABLE USER TYPES

Records and classes with comparison operators can be constrained:

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

The base type must have the operators used in the constraint expression.

NON-CONSTRAINABLE TYPES (E04010)

These types CANNOT be constrained:

  Boolean — already has only two values
  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

WHY THESE RESTRICTIONS

Constraint expressions need comparison operators to evaluate values. Boolean is already maximally constrained. JSON, traits, and abstract classes cannot participate in meaningful value comparisons.

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

    // === 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?}`)

      // === 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: types, constrain, constrainable, boolean, integer, string, built-in, E04010, date, money