Scala Sealed Traits
A sealed trait restricts which classes or objects can extend it — only those defined in the same file. This gives the compiler complete knowledge of every possible subtype. As a result, the compiler can check that your pattern matches cover every case and warn you when one is missing.
Why sealed?
Without sealed: With sealed:
──────────────────────────── ────────────────────────────────
trait Color sealed trait Color
// Any file can add subtypes // Only THIS file can add subtypes
case object Red extends Color case object Red extends Color
// Later, in another file: case object Green extends Color
case object Purple extends Color // case object Blue extends Color
// Compiler cannot know all
// subtypes — match is unsafe // Compiler knows: Red, Green, Blue
// Warns if match misses any!
Basic Sealed Trait
sealed trait Direction
case object North extends Direction
case object South extends Direction
case object East extends Direction
case object West extends Direction
def opposite(d: Direction): Direction = d match
case North => South
case South => North
case East => West
case West => East
// No _ needed — compiler knows all cases
println(opposite(North)) // South
println(opposite(East)) // West
Sealed Trait with Case Classes
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Triangle(base: Double, height: Double) extends Shape
def area(s: Shape): Double = s match
case Circle(r) => Math.PI * r * r
case Rectangle(w, h) => w * h
case Triangle(b, h) => 0.5 * b * h
val shapes: List[Shape] = List(Circle(3), Rectangle(4,5), Triangle(6,8))
shapes.foreach(s => println(f"${s.getClass.getSimpleName}: ${area(s)}%.2f"))
// Circle: 28.27
// Rectangle: 20.00
// Triangle: 24.00
sealed trait Shape
│
├── Circle (case class)
├── Rectangle (case class)
└── Triangle (case class)
Compiler knows EXACTLY these 3 subtypes.
Miss one in a match → compile warning.
Sealed Trait for State Machines
sealed trait OrderStatus
case object Pending extends OrderStatus
case object Confirmed extends OrderStatus
case object Shipped extends OrderStatus
case object Delivered extends OrderStatus
case class Cancelled(reason: String) extends OrderStatus
def nextStatus(current: OrderStatus): Option[OrderStatus] = current match
case Pending => Some(Confirmed)
case Confirmed => Some(Shipped)
case Shipped => Some(Delivered)
case Delivered => None // terminal state
case Cancelled(_) => None // terminal state
def statusLabel(s: OrderStatus): String = s match
case Pending => "⏳ Awaiting confirmation"
case Confirmed => "✅ Order confirmed"
case Shipped => "🚚 Out for delivery"
case Delivered => "📦 Delivered"
case Cancelled(r) => s"❌ Cancelled: $r"
var status: OrderStatus = Pending
for _ <- 1 to 4 do
println(statusLabel(status))
status = nextStatus(status).getOrElse(status)
println(statusLabel(status))
// ⏳ Awaiting confirmation
// ✅ Order confirmed
// 🚚 Out for delivery
// 📦 Delivered
// 📦 Delivered (stays Delivered)
sealed trait vs sealed abstract class
// sealed trait — cannot have constructor parameters (Scala 2)
sealed trait Event
// sealed abstract class — can have constructor parameters
sealed abstract class Event(val timestamp: Long)
case class LoginEvent(userId: Int, override val timestamp: Long) extends Event(timestamp)
case class LogoutEvent(userId: Int, override val timestamp: Long) extends Event(timestamp)
def handle(e: Event): String = e match
case LoginEvent(uid, ts) => s"User $uid logged in at $ts"
case LogoutEvent(uid, ts) => s"User $uid logged out at $ts"
The Compiler Warning in Action
sealed trait Light
case object Red extends Light
case object Yellow extends Light
case object Green extends Light
// Missing Yellow → compiler warning:
// "match may not be exhaustive. It would fail on pattern: Yellow"
def action(l: Light): String = l match
case Red => "Stop"
// case Yellow => "Caution" ← commented out on purpose
case Green => "Go"
// With _ fallback — suppresses warning but hides the gap:
def safeAction(l: Light): String = l match
case Red => "Stop"
case Green => "Go"
case _ => "Unknown" // ← hides the missing Yellow
The best practice is to handle every case explicitly rather than relying on case _. When you add a new subtype later, the compiler will immediately show you every match that needs updating.
Sealed Traits as Sum Types
Sealed trait hierarchies model what mathematicians call a sum type — a type that is exactly one of its variants. This is perfect for representing concepts that have a finite set of states:
sealed trait Result[+A]
case class Ok[A](value: A) extends Result[A]
case class Err(message: String) extends Result[Nothing]
def safeDivide(a: Int, b: Int): Result[Double] =
if b == 0 then Err("Division by zero")
else Ok(a.toDouble / b)
safeDivide(10, 2) match
case Ok(v) => println(f"Result: $v%.2f") // Result: 5.00
case Err(m) => println(s"Error: $m")
safeDivide(7, 0) match
case Ok(v) => println(f"Result: $v%.2f")
case Err(m) => println(s"Error: $m") // Error: Division by zero
