For the Hierarchy of Reals/Rationals/Integers/Naturals example we can do something like:
type Real = Rational | Irrational
object Real:
abstract class Type(computeDigits: Int => BigDecimal)
case class Irrational(computeDigits: Int => BigDecimal)
extends Real.Type(computeDigits)
type Rational = Integer | Fraction
object Rational:
abstract class Type(numerator: Int, denominator: Int)
extends Real.Type(_ => BigDecimal(numerator) / denominator)
case class Fraction(numerator: Int, denominator: Int)
extends Rational.Type(numerator, denominator)
enum Integer(n: Int) extends Rational.Type(n, 1):
case Natural(n: Int) extends Integer(n)
case Negative(n: Int) extends Integer(n)
val r1: Integer = Integer.Natural(4)
val r2: Integer = Integer.Negative(5)
val r3: Rational = Fraction(2, 4)
val r4: Irrational = Irrational(i => 2*i)
Here I introduced a common abstract class / trait for each union type that shares a common member and declare it inside an object with the same name as the union type.
As observed by @bjornregnell the union types with “shared members” relate to the discussion over at Making union types even more useful