Skip to Content
Course content

14: Sealed Traits and Enums (Scala 3)

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this all the time when developers move from Java or Python to Scala: they treat sealed trait as just a way to "lock down" a class hierarchy so other people can't extend it. They think it's a visibility or security feature. It's not.

It's Not Just About Restricting Inheritance

If you think sealed is just about preventing some random developer from adding a new subclass in another package, you're missing the actual superpower. Let's look at a payment system example. Imagine you have a trait for payment methods:

trait PaymentMethod
case class CreditCard(number: String) extends PaymentMethod
case class PayPal(email: String) extends PaymentMethod

Now, you write a function to process these payments using a pattern match. Everything looks great until you realize that because PaymentMethod is an open trait, the compiler has no idea how many subtypes actually exist. It has to assume there might be a CryptoPayment or BankTransfer defined in some other file you've never seen.

def process(method: PaymentMethod) = method match {
  case CreditCard(n) => println(s"Charging card $n")
  case PayPal(e) => println(s"Redirecting to $e")
}

If you run this, the compiler stays silent. But if you suddenly add a BankTransfer class later, the code above will throw a MatchError at runtime. That's a production outage waiting to happen.

Letting the Compiler Do Your Testing

This is where sealed comes in. When you mark a trait as sealed, you're telling the Scala compiler: "Every single implementation of this trait is defined in this one file."

sealed trait PaymentMethod
case class CreditCard(number: String) extends PaymentMethod
case class PayPal(email: String) extends PaymentMethod

Now, try that same process function again. The compiler will give you a warning: "match may not be exhaustive". It knows exactly what the possibilities are. I personally rely on this more than almost any other feature in Scala; it effectively turns your compiler into a unit test that ensures you've handled every single edge case whenever you add a new type to your domain.

The Scala 3 Way: Using Enums for Conciseness

In Scala 2, we had to write the sealed trait and case object/class boilerplate every time. In Scala 3, we have enum. Now, don't confuse these with Java enums. Scala enums are actually just a concise way to define a sealed trait hierarchy.

If you just need a simple set of tags, it looks like this:

enum OrderStatus {
  case Pending, Shipped, Delivered, Cancelled
}

Under the hood, Scala creates a sealed trait called OrderStatus and four singleton objects. It's clean, it's fast, and it still gives you that beautiful exhaustiveness checking in your match statements.

When Enums Need More Than Just a Name

The real magic happens when your enum cases need to hold data. You can mix case objects and case classes directly inside the enum definition. I use this constantly for representing API responses or state machine transitions.

enum NetworkResponse {
  case Success(data: String)
  case Failure(errorCode: Int, message: String)
  case Loading
}

Notice how Loading is essentially a case object while the others are case classes. You get the best of both worlds: the brevity of an enum and the power of a full class hierarchy. You're essentially building what functional programmers call an Algebraic Data Type (ADT). It sounds academic, but in practice, it just means "a type that is exactly one of these few things."




📋 Practical Task

Building a Document Approval Workflow

You are building a system to track the lifecycle of a legal document. A document can be in one of the following states:

  • Draft: Needs an author's name.
  • UnderReview: Needs the name of the reviewer and a timestamp.
  • Approved: Needs a signature string.
  • Rejected: Needs a reason for rejection.

Your Task:

  1. Create an enum called DocumentState that captures these four states with their respective data.
  2. Write a function called describeState that takes a DocumentState and returns a human-readable string describing the status (e.g., "Document is currently being reviewed by Alice").
  3. Ensure that your describeState function uses a match expression.
  4. To verify the exhaustiveness check: temporarily comment out one of the cases in your match block and observe the compiler warning. Then, uncomment it to fix the warning.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.