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

#!ek9
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: api, boundary, constrained, construction, function, guard, parameter, type-safe, validate