Scala 3 FAQ: What are opaque types in Scala?
Discussion
I previously wrote a little about Opaque Types in Scala 3, and today, as I’m working on a new video about opaque types, I thought I’d add some more information about them.
Note: I created this article in part through interactions with ChatGPT and Google Gemini, while developing my Advanced Scala 3 video course.
Scala 3 Opaque Types
In Scala 3, opaque types are used to provide a way to define abstract data types with controlled access to their underlying representation. You should consider using opaque types in the following scenarios:
-
Encapsulation: When you want to encapsulate the underlying representation of a type and only expose specific operations on that type. (Such as creating a
Money
type, whose internal representation isBigDecimal
.) -
Type Safety: When you want to ensure type safety by restricting the operations that can be performed on a particular type, opaque types can help prevent accidental misuse.
-
Abstraction: Related to the first two points, when you want to create new data types without exposing their internal representation, opaque types allow you to define clear interfaces while hiding implementation details.
-
Semantic Clarity: When you want to improve the readability and clarity of your code by giving meaningful names to types — such as
EmailAddress
instead ofString
— opaque types allow you to create new types with descriptive names. This is consistent with practices of Domain-Driven Design (DDD) and Functional Programming (FP).
Note that in DDD and FP, instead of using the Scala String
type to represent an email address, programmers will often create an EmailAddress
type to achieve the benefits just stated. This approach is very similar to the following opaque type example.
Scala 3 Opaque Type Example
Here’s a Scala 3 example to illustrate the use of opaque types:
opaque type Kilometers = Double
object Kilometers:
def apply(value: Double): Kilometers =
assert(value >= 0, "Kilometers value cannot be negative")
value
extension (km: Kilometers)
def toMiles: Miles = Miles(km * 0.621371)
end Kilometers
opaque type Miles = Double
object Miles:
def apply(value: Double): Miles =
assert(value >= 0, "Miles value cannot be negative")
value
extension (miles: Miles)
def toKilometers: Kilometers = Kilometers(miles / 0.621371)
end Miles
// usage:
val distanceInKm: Kilometers = Kilometers(100)
val distanceInMiles: Miles = distanceInKm.toMiles
println(s"$distanceInKm kilometers is $distanceInMiles miles.")
In this example, we’ve defined opaque types Kilometers
and Miles
to represent distances. By using opaque types, we ensure that distances are always non-negative, and we provide specific conversion methods between kilometers and miles, using Scala 3 extension methods. This encapsulation, type safety, abstraction, and semantic clarity improve the robustness, safety, and clarity of our code.
More information
For more information and examples, see my free Scala 3 opaque types training video.
Alternative
If you like Scala 3 opaque types, check out the Neotype project as an improvement over opaque types.