Opaque Types
Zero-cost abstractions.
Opaque Types is a free Scala for Backend Engineering & Functional Programming lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Scala for Backend Engineering & Functional Programming learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
What Are Opaque Types?
Opaque types are a Scala 3 feature for creating zero-cost abstractions. They give a distinct type at compile time but compile down to the underlying type with no runtime wrapping.
- Type safety of a wrapper class.
- Zero allocation overhead.
object Distances:
opaque type Meters = Double
object Main:
def main(args: Array[String]): Unit =
println("Opaque types compile to their underlying type")Declaring an Opaque Type
Declare an opaque type inside an object or class. Outside that scope, Meters and Double are treated as different types.
object Distances:
opaque type Meters = Double
def meters(d: Double): Meters = d
def toDouble(m: Meters): Double = m
object Main:
def main(args: Array[String]): Unit =
val d = Distances.meters(5.0)
println(Distances.toDouble(d))The Boundary of Opacity
Inside the defining scope, the opaque type and its underlying type are interchangeable. Outside, only the opaque type is visible, so you cannot accidentally pass a raw Double where Meters is required.
object Distances:
opaque type Meters = Double
def apply(d: Double): Meters = d
def show(m: Meters): String = s"$m m"
object Main:
def main(args: Array[String]): Unit =
val m = Distances(10.0)
println(Distances.show(m))Adding Extension Methods
Give opaque types behavior with extension methods defined in the same scope. Callers get convenient operations without exposing the underlying type.
object Distances:
opaque type Meters = Double
def apply(d: Double): Meters = d
extension (m: Meters)
def +(other: Meters): Meters = m + other
def value: Double = m
object Main:
def main(args: Array[String]): Unit =
val total = Distances(3.0) + Distances(4.0)
println(total.value)Preventing Mixups
The big win: two opaque types over the same underlying type are incompatible. Meters cannot be confused with Kilometers, even though both are Doubles.
object Units:
opaque type Meters = Double
opaque type Kilometers = Double
def m(d: Double): Meters = d
def km(d: Double): Kilometers = d
def toMeters(k: Kilometers): Meters = k * 1000
object Main:
def main(args: Array[String]): Unit =
val far = Units.km(2.0)
println(Units.toMeters(far))Opaque Types with Bounds
You can give an opaque type an upper bound with <:. This exposes part of the public API while keeping the exact representation hidden.
object Ids:
opaque type UserId <: Int = Int
def apply(i: Int): UserId = i
object Main:
def main(args: Array[String]): Unit =
val id = Ids(99)
println(id + 1)Smart Construction with Validation
Combine opaque types with a validating factory to ensure values are always legal. The constructor stays private; only the validated factory is public.
object Ages:
opaque type Age = Int
def of(i: Int): Option[Age] =
if i >= 0 && i < 150 then Some(i) else None
extension (a: Age) def value: Int = a
object Main:
def main(args: Array[String]): Unit =
println(Ages.of(30).map(_.value))
println(Ages.of(-1))Zero Runtime Cost
Unlike a value class or case class wrapper, an opaque type creates no object at runtime. A List of Meters is literally a list of doubles in the JVM, so there is no boxing.
object Temps:
opaque type Celsius = Double
def c(d: Double): Celsius = d
extension (t: Celsius) def value: Double = t
object Main:
def main(args: Array[String]): Unit =
val readings = List(Temps.c(20.0), Temps.c(21.5))
println(readings.map(_.value).sum)Opaque vs Type Alias
A plain type X = Y alias is transparent: X and Y are fully interchangeable everywhere. An opaque type hides that equality outside its scope, giving real type safety.
object Demo:
type Name = String // transparent alias
opaque type Email = String // opaque
def email(s: String): Email = s
extension (e: Email) def raw: String = e
object Main:
def main(args: Array[String]): Unit =
val e = Demo.email("a@b.com")
println(e.raw)Opaque vs Case Class Wrapper
A case class wrapper like case class Meters(value: Double) also adds type safety but allocates an object. Opaque types give the same safety with the performance of the raw type.
object Money:
opaque type Cents = Long
def cents(n: Long): Cents = n
extension (c: Cents)
def +(o: Cents): Cents = c + o
def value: Long = c
object Main:
def main(args: Array[String]): Unit =
val total = Money.cents(150) + Money.cents(50)
println(total.value)When to Use Opaque Types
Use opaque types when you want strong domain typing in hot paths or large collections.
- Distinguish quantities (Meters vs Seconds).
- Enforce validated invariants.
- Avoid allocation overhead of wrappers.
object Geo:
opaque type Latitude = Double
def lat(d: Double): Option[Latitude] =
if d >= -90 && d <= 90 then Some(d) else None
extension (l: Latitude) def value: Double = l
object Main:
def main(args: Array[String]): Unit =
println(Geo.lat(41.0).map(_.value))Quick Check
Test your understanding of opaque types.
Recap
You learned Scala 3 opaque types.
opaque type X = Yhides the equality outside its scope.- Provide factories and
extensionmethods for the API. - Different opaque types over the same base are incompatible.
- Optional upper bounds with
<:expose partial API. - Zero runtime cost, unlike case class wrappers.
object Ids:
opaque type OrderId = String
def apply(s: String): OrderId = s
extension (o: OrderId) def raw: String = o
object Main:
def main(args: Array[String]): Unit =
val id = Ids("ORD-1")
println(id.raw)Frequently asked questions
Is the “Opaque Types” lesson free?
Yes — the full text of “Opaque Types” is free to read here on the web, and the Scala for Backend Engineering & Functional Programming course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Scala for Backend Engineering & Functional Programming course, upgrade to CoddyKit PRO.
What will I learn in “Opaque Types”?
Zero-cost abstractions. You practise Scala for Backend Engineering & Functional Programming with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Scala for Backend Engineering & Functional Programming?
No prior experience is required. Scala for Backend Engineering & Functional Programming on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Opaque Types” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Scala for Backend Engineering & Functional Programming lesson?
Yes. Every Scala for Backend Engineering & Functional Programming lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.