Create me a constrained EmailAddress type as String validated by a regex, then write a registerUser function that uses a guard 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 declaration guards 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 declaration guard 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. A function taking EmailAddress receives either a valid value or an unset one, never an invalid set value. Checking with a guard, as above, is the recommended pattern, though the compiler does not force the check.
3. No exceptions for predictable input failures — unset is the natural representation.
See Q269 for input validation patterns. See Q257 for constrained type basics. See Q74 for declaration guards.
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 — The base type of a constrained type must be a type the compiler can resolve. 'UnknownBase' is not declared anywhere, so E50010 is reported on that line. (A user-defined record or class is a valid base only when it is copyable and provides the operator the constraint uses.) See ek9 -h E50010 for details.
Incorrect:
EmailAddress as UnknownBase 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 guards for type-safe validation at the boundary.
Keywords: matches, constrained type, guard, domain type, regex, validation, newtype, email