How do constrained types relate to the type hierarchy?
← Advanced Type System · Ref: Q722
Constrained types are deliberately disconnected from the type hierarchy. They share operators with their base type but are NOT subtypes.
LIKE-A NOT IS-A
DrivingAge shares Integer's operators but is not an Integer:
age <- DrivingAge(25) //intVar as Integer: age //compile error: INCOMPATIBLE_TYPES intVar <- #^ age //correct: use promote to cross boundary
This is a LIKE-A relationship: DrivingAge behaves like an Integer but is a separate type.
SUPER IS ANY
In the type hierarchy, constrained types have Any as their super type, not the base type:
DrivingAge -> Any (not DrivingAge -> Integer -> Any)
This prevents polymorphic substitution that would bypass validation.
CANNOT CONSTRAIN A CONSTRAINED TYPE
Constraining is a deliberately SIMPLE, one-level concept: a base type limited to a restricted range of values. You cannot constrain (or alias) an already-constrained type — once constrained it is its own distinct type and is no longer a candidate to be constrained. The compiler raises TYPE_CANNOT_BE_CONSTRAINED (E04010):
Index as Integer constrain as > 0 //DBIndex as Index constrain as < 1000000 //compile error E04010: 'Index' is not a candidate to be constrained
To make a more specific type, constrain the BASE type directly with the combined constraint:
BoundedIndex as Integer constrain as > 0 and < 1000000
CANNOT EXTEND VIA INHERITANCE
Constrained types are closed — you cannot extend them with 'extends' or 'as open', nor constrain/alias them further. The only way to create a more specific type is to declare a new constrained type over the BASE type with the combined constraint.
EXPLICIT CONVERSION
To cross type boundaries, use constructors or promote:
age <- DrivingAge(25) rawInt <- #^ age //promote to Integer newAge <- DrivingAge(rawInt) //construct from Integer
See Q257 for constrained type overview. See Q718 for promote operator. See Q101 for closed-by-default types.
Example
defines module qa.advancedtypes.constrainedhierarchy defines type // === BASE CONSTRAINED TYPE === Index as Integer constrain as > 0 // === A MORE SPECIFIC TYPE: constrain the BASE type directly (NOT 'as Index') === //You cannot constrain an already-constrained type — combine the constraints over the base type. BoundedIndex as Integer constrain as > 0 and < 1000000 DrivingAge as Integer constrain as >= 16 and <= 100 defines function <?- Takes raw Integer — not DrivingAge. -?> processRawInteger() as pure -> intValue as Integer <- result as String: `Integer value: ${intValue}` defines program ConstrainedHierarchyDemo() stdout <- Stdout() // === LIKE-A NOT IS-A === age <- DrivingAge(25) stdout.println(`DrivingAge: ${age}`) //DrivingAge is NOT an Integer — must promote rawInt <- #^ age output <- processRawInteger(rawInt) stdout.println(output) // === CANNOT CONSTRAIN A CONSTRAINED TYPE: constrain the base directly === idx <- Index(42) stdout.println(`Index: ${idx}`) bIdx <- BoundedIndex(500) stdout.println(`BoundedIndex: ${bIdx}`) //Out-of-range via the fallible factory (never panics — returns an unset value) badIdx <- BoundedIndex().of(0) stdout.println(`BoundedIndex(0) valid: ${badIdx?}`) // === PROMOTE WORKS ON ALL CONSTRAINED TYPES === rawFromIndex <- #^ idx stdout.println(`Index promoted to Integer: ${rawFromIndex}`) // === CONSTRUCTION CROSSES BOUNDARY === newAge <- DrivingAge(30) rawAge <- #^ newAge reconstructed <- DrivingAge(rawAge) stdout.println(`Round-trip: ${reconstructed}`) // === LIST OF CONSTRAINED TYPE WORKS === ages <- List() of DrivingAge ages += DrivingAge(25) ages += DrivingAge(30) ages += DrivingAge(40) stdout.println(`Ages count: ${length ages}`)
Common mistakes
E04010 — You cannot constrain an already-constrained type (Index is itself constrained). Constraining is one-level — constrain the base type directly with the combined constraint. See ek9 -h E04010 for details.
Incorrect:
DBIndex as Index constrain as < 1000000
Correct:
BoundedIndex as Integer constrain as > 0 and < 1000000
E50060 — Index has no intValue() method in EK9. Use the promote operator (#^) to extract the base type value. See ek9 -h E50060 for details.
Incorrect:
rawFromIndex <- idx.intValue()
Correct:
rawFromIndex <- #^ idx
Other ways to ask this
- Why is a constrained type not a subtype of its base type?
- What does LIKE-A mean for constrained types?
- Can I constrain a constrained type?
Coming from another language?
Java: wrapper classes are subtypes of Object, custom value wrappers can be subtypes of base class. Python: no type hierarchy constraints, duck typing. Rust: newtype pattern creates a separate type (like EK9), explicit From/Into for conversions. Go: type definitions create new types disconnected from base (similar to EK9). Kotlin: value classes are still subtypes of their underlying type. Swift: no newtype pattern, typealias shares identity. EK9: constrained types are LIKE-A (share operators) not IS-A (not subtypes) — super is Any, fully disconnected, and constraining is strictly one-level: you constrain a base type, never another constrained type.
Keywords: alias, hierarchy, explicit, E04010, constrained, boundary, extend, like-a, subtype, disconnected, any