Choosing Between Scala 3 `enum` and Sealed Traits for Closed Case Sets
Decide whether to use Scala 3's built‑in enum or a sealed trait for a closed set of cases. The article compares features, trade‑offs, and shows a concrete example with validation steps.
27 Aug 2026, 06:32 UTC

When to Use a Scala 3 `enum` Instead of a Sealed Trait
In Scala 3 you can model a closed set of cases with two idioms:
- the built‑in
enumdeclaration - a hand‑written sealed trait and case classes/objects
Decision Matrix
| Feature | Scala 3 enum | Sealed trait + case classes |
|---|---|---|
| Closed‑world intent | Explicit – compiler knows all cases exist in the same file | Implicit – sealed trait enforces no subclasses outside the file but still requires manual listing for some tooling |
| Case‑level type parameters | All cases share the same type parameters; heterogeneous per‑case types need a sealed trait | Each case can have its own type parameters and companion objects |
| Separate compilation | All cases compiled together; cannot split across files | Cases can live in separate files, useful for large hierarchies |
| Generated API | Automatic values, valueOf, ordinal and exhaustivity checks | Must hand‑roll helpers or use libraries (e.g., Enumeratum) |
| Java interop | Compiles to java.lang.Enum for simple, parameterless cases; otherwise plain classes | Plain classes – no special enum encoding |
| Serialization support | Libraries like Circe provide automatic derivation for enums; some may require custom codecs | Automatic derivation works but may need explicit @JsonCodec or similar |
| Binary compatibility | Adding a case breaks exhaustive matches and may affect generated values; deprecating is harder | Can deprecate or shim old cases with separate objects; more flexibility |
| Tooling & reflection | Reflection tools may not recognize enum cases without updates | Standard sealed trait reflection works out of the box |
Trade‑off Summary
- Use
enumwhen: you have a small, stable set of homogeneous cases, you want the compiler to generate boilerplate, and you can keep all cases in one file. - Use a sealed trait when: you need per‑case type parameters, separate compilation, fine‑grained companion logic, or you want more control over binary compatibility.
Concrete Example
Suppose we need a closed set of traffic lights. The enum approach is concise:
enum TrafficLight(val color: String) {
case Red extends TrafficLight("red")
case Yellow extends TrafficLight("yellow")
case Green extends TrafficLight("green")
}
With the sealed trait approach you write:
sealed trait TrafficLight(val color: String)
object TrafficLight {
case object Red extends TrafficLight("red")
case object Yellow extends TrafficLight("yellow")
case object Green extends TrafficLight("green")
}
Both compile to similar bytecode, but the enum adds a values array automatically. You can validate exhaustivity by writing a match that omits one case:
def describe(t: TrafficLight) = t match {
case TrafficLight.Red => "Stop"
case TrafficLight.Yellow => "Caution"
// Missing Green triggers a compiler error
}
Compile with scalac -Xfatal-warnings and confirm the compiler reports a missing case. For the sealed trait you get the same warning.
Validation Checklist
- Compile-time exhaustivity: Use
-Xfatal-warningsto ensure missing cases are caught. - Generated helpers: Run
scalac -Xprint:typeror decompile the class to verifyvaluesandvalueOfexist for enums. - Serialization round‑trip: Serialize to JSON with Circe, deserialize back, and compare the original value. For enums Circe derives codecs automatically; for sealed traits you may need
@JsonCodecor a customEncoder/Decoder. - Java interop: In a Java file call
TrafficLight.valueOf("RED")(for a parameterless enum) orTrafficLight.Redto confirm visibility. - Binary compatibility: Add a new case to the enum or sealed trait in a separate compilation step and run
scalac -Xlint:unusedon downstream code to see if exhaustive matches break.
Limitations and Practical Tips
- Enums cannot model heterogeneous per‑case type parameters. If you need
sealed trait ShapewithCircle(radius: Double)andRectangle(width: Double, height: Double), use a sealed trait. - Enum syntax changed between Scala 3 minor releases (e.g.,
enumwithextendsvsenumwithcasesyntax). Verify against the exact compiler version in yourbuild.sbt. - Libraries that rely on reflection (e.g.,
shapeless.Typeable) may need updates to recognize enum cases. - When publishing a library, avoid changing an enum after release because downstream code may have compiled against the old
valuesarray.
Conclusion
Scala 3’s enum is a powerful tool for small, homogeneous closed sets, giving you zero‑boilerplate and compiler support. For more complex hierarchies, per‑case logic, or when you need to split code across files, a sealed trait remains the safer choice. Use the decision matrix and validation checklist above to pick the right model for your project.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.