Create me a constrained EmailAddress type as String validated by a regex, then write a registerUser function that uses a guard expression to accept only valid emails.

← Security and Sanitization · Ref: Q1241

EK9 constrained types let you build domain types from existing primitive types with a validation rule. There are two construction paths. A BARE constructor — EmailAddress(value) — ASSERTS the value is valid: a set value that violates the regex PANICS at runtime (and an invalid literal constant is the compile error E08260, so it never reaches runtime). For UNTRUSTED/boundary input use the fallible factory EmailAddress().of(value), which returns an UNSET instance on failure — not an exception — and composes cleanly with guard expressions to filter bad data at the boundary.

DEFINING THE CONSTRAINED TYPE

  defines type
    EmailAddress as String constrain as
      matches /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/

The type is declared in a 'defines type' section. The fallible factory (e.g. EmailAddress().of("alice@example.com")) attempts to validate the regex; if it fails, the result is unset.

FUNCTION USING THE TYPE

  registerUser() as pure
    ->
      userName as String
      email as EmailAddress
    <- rtn as String: `Registered ${userName} with ${email}`

The function takes EmailAddress (not String). The type system makes it impossible to call this function with an unvalidated value.

GUARD AT THE CALL SITE

Use the fallible factory in a guard expression to attempt construction and only call the function on success:

  if validEmail <- EmailAddress().of("alice@example.com")
    msg <- registerUser("Alice", validEmail)
    stdout.println(msg)
  else
    stdout.println("Invalid email format")
  if invalidEmail <- EmailAddress().of("not-an-email")
    msg <- registerUser("Bob", invalidEmail)
    stdout.println(msg)
  else
    stdout.println("Invalid email format")

The '.of(value)' factory returns an unset EmailAddress when validation fails (a bare EmailAddress(value) would PANIC on a set invalid value instead). The block under 'if' only runs when construction succeeded. The 'else' handles the rejected case explicitly.

KEY ADVANTAGES

1. Validation lives in the TYPE, not scattered through call sites.
2. Functions taking EmailAddress are guaranteed to receive valid values.
3. No exceptions for predictable input failures — unset is the natural representation.
4. The compiler enforces that you check 'isSet' before use.

See Q269 for input validation patterns. See Q257 for constrained type basics. See Q74 for guard expressions.

Example

defines module qa.security.emailtype

  defines type

    EmailAddress as String constrain as
      matches /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/

  defines function

    registerUser() as pure
      ->
        userName as String
        email as EmailAddress
      <- rtn as String: `Registered ${userName} with ${email}`

  defines program

    EmailValidationDemo()
      stdout <- Stdout()

      if validEmail <- EmailAddress().of("alice@example.com")
        msg <- registerUser("Alice", validEmail)
        stdout.println(msg)
      else
        stdout.println("Invalid email format")

      if invalidEmail <- EmailAddress().of("not-an-email")
        msg <- registerUser("Bob", invalidEmail)
        stdout.println(msg)
      else
        stdout.println("Invalid email format")

Common mistakes

E50010 — Constrained types can only be built from primitive types like String, Integer or Float. User-defined records and classes cannot be constrained because they lack the comparison and pattern-matching semantics required. See ek9 -h E50010 for details.

Incorrect:

EmailAddress as UserRecord constrain as
      matches /^.+@.+$/

Correct:

EmailAddress as String constrain as
      matches /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
Other ways to ask this
  • Define an EmailAddress constrained type and use it with a guard.
  • Show me a validated email type that is unset on bad input.
  • Implement a constrained String type with regex matching for emails.
  • Write a registration function that rejects malformed email addresses.

Coming from another language?

Java: validate-on-setter or Bean Validation @Email annotation. Kotlin: Result<Email> or sealed Either type. Rust: newtype struct Email(String) with try_from validation. Scala: Refined types or smart constructors. Python: pydantic EmailStr or manual regex check. EK9: 'defines type X as String constrain as matches /regex/' then X().of(value) produces unset on validation failure, composing with guard expressions for type-safe validation at the boundary.

Keywords: regex, domain type, guard, constrained type, matches, email, newtype, validation