How do I use constrained types as function parameters for type safety?

← Advanced Type System · Ref: Q721

Constrained types create type-safe function boundaries that eliminate an entire category of invalid-argument bugs at compile time.

DISCONNECTED TYPES ENFORCE API CONTRACTS

When a function takes a constrained type, you cannot pass the raw base type:

  printName()
    -> name as Name
    //...
  printName("Alice")          //compile error: String is not Name
  printName(Name("Alice"))    //correct: construct the constrained type

CONSTRUCTION IS THE VALIDATION BOUNDARY

The caller must construct the constrained type, which validates untrusted input via the fallible factory:

  if validName <- Name().of(userInput)
    printName(validName)       //guaranteed valid

If the input violates the constraint, of() yields an unset value and the guard's else branch handles it. A bare constructor instead ASSERTS validity — it Panics on a set out-of-range value (and a violating literal is the compile error E08260) — so use of() for untrusted input.

GUARD AT THE BOUNDARY

Use the guard pattern to validate at system entry points:

  if age <- DrivingAge().of(rawInput)
    processDriver(age)         //DrivingAge is validated
  else
    stdout.println("Invalid age")

Inside processDriver, the DrivingAge is guaranteed to be in range.

CONSTRAINED TYPES IN CLASS PROPERTIES

Classes with constrained type fields enforce validity at construction:

  Book
    title as BookTitle?
    author as AuthorName?

Every Book instance has a validated title and author.

See Q257 for constrained type overview. See Q269 for input validation patterns. See Q722 for type hierarchy.

Example

defines module qa.advancedtypes.constrainedparameters

  defines type

    Name as String constrain as
      matches /^[a-zA-Z -]+$/

    DrivingAge as Integer constrain as
      >= 16 and <= 100

    BookTitle as String constrain as
      matches /^[a-zA-Z0-9 :'-]+$/

  defines function

    <?-
      Takes a validated Name — cannot accept raw String.
    -?>
    formatGreeting() as pure
      -> name as Name
      <- greeting as String: `Hello, ${name}!`

    <?-
      Takes a validated DrivingAge — cannot accept raw Integer.
    -?>
    canRentCar() as pure
      -> age as DrivingAge
      <- allowed as Boolean?
      rentalMinimum <- 21
      allowed: age >= rentalMinimum

  defines program

    ConstrainedParametersDemo()
      stdout <- Stdout()

      // === MUST CONSTRUCT CONSTRAINED TYPE FIRST ===
      validName <- Name("Alice Smith")
      greeting <- formatGreeting(validName)
      stdout.println(greeting)

      // === GUARD PATTERN AT BOUNDARY (fallible factory for untrusted input) ===
      userInput <- "Bob Jones"

      if checkedName <- Name().of(userInput)
        result <- formatGreeting(checkedName)
        stdout.println(result)
      else
        stdout.println("Invalid name provided")

      // === CONSTRAINED TYPE VALIDATES AT THE BOUNDARY ===
      if driverAge <- DrivingAge().of(25)
        canRent <- canRentCar(driverAge)
        stdout.println(`Age 25, can rent: ${canRent}`)

      // === INVALID INPUT IS REJECTED: of() RETURNS UNSET (a bare DrivingAge(10) would Panic) ===
      invalidAge <- DrivingAge().of(10)
      stdout.println(`Age 10 valid: ${invalidAge?}`)

      // === CONSTRAINED FIELDS IN RECORDS ===
      title <- BookTitle("The Great Adventure")
      stdout.println(`Book title: ${title}`)

      // === MULTIPLE CONSTRAINED PARAMETERS ===
      validTitle <- BookTitle("EK9 Guide")
      stdout.println(`Title valid: ${validTitle?}`)

Common mistakes

E50060 — String has no toUpperCase() method in EK9. Use upperCase() instead. See ek9 -h E50060 for details.

Incorrect:

greeting <- formatGreeting(validName).toUpperCase()

Correct:

greeting <- formatGreeting(validName)

E50060 — String has no toUpperCase() method in EK9. Use upperCase() instead. See ek9 -h E50060 for details.

Incorrect:

result <- formatGreeting(checkedName).toUpperCase()

Correct:

result <- formatGreeting(checkedName)
Other ways to ask this
  • Why should I use constrained types in function signatures?
  • Can I pass a String where a Name is expected?
  • How do constrained types replace runtime validation?

Coming from another language?

Java: parameter validation via Bean Validation annotations (@Valid, @NotNull), checked at runtime. Python: type hints are advisory, validation with pydantic at runtime. Rust: newtype pattern enforces boundaries but requires manual From/Into implementations. Go: no type constraints, runtime validation in function body. Kotlin: value classes provide some type safety but still subtypes. EK9: constrained types as parameters eliminate invalid-argument bugs at compile time — disconnected types prevent passing raw values, construction validates constraints, guard pattern handles boundary validation.

Keywords: type-safe, function, boundary, construction, validate, constrained, parameter, api, guard