scalaadvanced

Opaque Types for Type Safety

Use Scala 3 opaque types to create zero-cost type wrappers for domain modeling.

scala
// Opaque types: zero-cost type wrappers
object Types:
  opaque type UserId = Long
  opaque type OrderId = Long
  opaque type Email = String
  opaque type NonEmptyString = String
  opaque type Percentage = Double

  object UserId:
    def apply(id: Long): UserId = id
    extension (id: UserId)
      def value: Long = id
      def next: UserId = id + 1

  object OrderId:
    def apply(id: Long): OrderId = id
    extension (id: OrderId)
      def value: Long = id

  object Email:
    def apply(value: String): Either[String, Email] =
      if value.matches(".+@.+\\..+") then Right(value)
      else Left(s"Invalid email: $value")

    def unsafe(value: String): Email = value

    extension (email: Email)
      def value: String = email
      def domain: String = email.split("@").last
      def local: String = email.split("@").head

  object NonEmptyString:
    def apply(s: String): Option[NonEmptyString] =
      if s.nonEmpty then Some(s) else None

    extension (s: NonEmptyString)
      def value: String = s
      def length: Int = s.length

  object Percentage:
    def apply(value: Double): Either[String, Percentage] =
      if value >= 0 && value <= 100 then Right(value)
      else Left(s"$value is not a valid percentage")

    extension (p: Percentage)
      def value: Double = p
      def asFraction: Double = p / 100.0
      def format: String = f"$p%.1f%%"

import Types.*

// Can't accidentally mix UserId and OrderId
def getOrder(userId: UserId, orderId: OrderId): String =
  s"User ${userId.value} -> Order ${orderId.value}"

def sendEmail(to: Email, subject: NonEmptyString): Unit =
  println(s"Sending '$subject' to ${to.value}")

@main def run(): Unit =
  val userId = UserId(42)
  val orderId = OrderId(100)

  // This would NOT compile:
  // getOrder(orderId, userId)  // type mismatch!
  println(getOrder(userId, orderId))

  // Validated construction
  Email("alice@test.com") match
    case Right(email) =>
      println(s"Domain: ${email.domain}")
      println(s"Local: ${email.local}")
    case Left(err) =>
      println(err)

  Email("not-an-email") match
    case Left(err) => println(s"Rejected: $err")
    case _ => ()

  for
    pct <- Percentage(75.5)
  do
    println(s"${pct.format} = ${pct.asFraction}")

  // NonEmptyString
  NonEmptyString("Hello").foreach { s =>
    println(s"Non-empty: $s (len=${s.length})")
  }
  println(s"Empty: ${NonEmptyString("")}")

Use Cases

  • Domain-driven type safety
  • Preventing primitive obsession
  • Zero-cost validated wrappers

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.