scalaintermediate

Union and Intersection Types

Use Scala 3 union and intersection types for flexible and precise type definitions.

scala
// Union types: A | B
def processInput(input: String | Int | Double): String = input match
  case s: String => s"String: $s"
  case i: Int    => s"Int: $i"
  case d: Double => s"Double: $d"

// Union in return type
def divide(a: Int, b: Int): Int | String =
  if b == 0 then "Division by zero"
  else a / b

// Error types with union
case class NotFound(id: String)
case class Forbidden(reason: String)
case class BadRequest(message: String)

type ApiError = NotFound | Forbidden | BadRequest

def handleError(error: ApiError): (Int, String) = error match
  case NotFound(id)      => (404, s"Not found: $id")
  case Forbidden(reason) => (403, s"Forbidden: $reason")
  case BadRequest(msg)   => (400, s"Bad request: $msg")

// Intersection types: A & B
trait Printable:
  def print(): String

trait Serializable:
  def serialize(): Array[Byte]

trait Validatable:
  def isValid: Boolean

case class Document(title: String, content: String)
    extends Printable with Serializable with Validatable:
  def print(): String = s"[$title] $content"
  def serialize(): Array[Byte] = s"$title|$content".getBytes
  def isValid: Boolean = title.nonEmpty && content.nonEmpty

// Function requiring intersection
def processDocument(doc: Printable & Serializable & Validatable): String =
  if doc.isValid then
    val printed = doc.print()
    val bytes = doc.serialize()
    s"Valid: $printed (${bytes.length} bytes)"
  else
    "Invalid document"

// Structural types
type HasName = { def name: String }
type HasAge = { def age: Int }
type Person = HasName & HasAge

@main def run(): Unit =
  // Union types
  println(processInput("hello"))
  println(processInput(42))
  println(processInput(3.14))

  // Return union
  val r1 = divide(10, 3)
  val r2 = divide(10, 0)
  println(s"10/3 = $r1")
  println(s"10/0 = $r2")

  // API errors
  val errors: List[ApiError] = List(
    NotFound("user-123"),
    Forbidden("insufficient permissions"),
    BadRequest("missing required field")
  )
  errors.map(handleError).foreach { (code, msg) =>
    println(s"  $code: $msg")
  }

  // Intersection
  val doc = Document("README", "Hello World")
  println(processDocument(doc))

  val emptyDoc = Document("", "")
  println(processDocument(emptyDoc))

Use Cases

  • Flexible function parameters
  • Typed error handling
  • Composable interface requirements

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.