Advanced Types

Opaque types are erased to the underlying type at runtime — no boxing.

28.1 Union types (Scala 3)

def parse(s: String): Int | String =
  s.toIntOption.getOrElse(s"invalid: $s")

parse("42") match
  case n: Int => println(n + 1)
  case err: String => println(err)

type Id = Int | String

28.2 Intersection types

trait Resettable { def reset(): Unit }
trait Growable[A] { def add(a: A): Unit }

def clearAndFill(x: Resettable & Growable[String]): Unit =
  x.reset()
  x.add("fresh")

28.3 Opaque types — zero-cost type safety

Opaque types are erased to the underlying type at runtime — no boxing.

object Units:
  opaque type Meters = Double
  object Meters:
    def apply(d: Double): Meters = d
  extension (m: Meters)
    def +(o: Meters): Meters = m + o
    def value: Double = m

  opaque type Email = String
  object Email:
    def from(s: String): Option[Email] = Option.when(s.contains("@"))(s)

import Units.*
val d = Meters(3.0) + Meters(4.5)
// val x: Double = d        // error: Meters is not Double outside Units

28.4 Literal & singleton types

val one: 1 = 1
def onlyTrue(b: true): Unit = ()
val s: "hello" = "hello"
summon[ValueOf[42]].value        // 42

28.5 Structural types

Uses reflection (slow); Scala 3 also supports custom Selectable for typed records.

import scala.reflect.Selectable.reflectiveSelectable
def close(r: { def close(): Unit }): Unit = r.close()

28.6 Path-dependent types

class Graph:
  class Node(val id: Int)
  def node(i: Int) = Node(i)
  def connect(a: Node, b: Node) = ()

val g1 = Graph(); val g2 = Graph()
val n1 = g1.node(1)
g1.connect(n1, g1.node(2))
// g1.connect(n1, g2.node(3))     // error: g2.Node is not g1.Node

28.7 Abstract type members vs type parameters

trait Container:
  type Elem
  def get: Elem

class IntBox extends Container:
  type Elem = Int
  def get = 42

28.8 Explicit nulls (-Yexplicit-nulls)

val s: String = null           // error
val t: String | Null = null
if t != null then t.length     // flow typing narrows to String

28.9 Phantom types & type-safe builders

sealed trait State; sealed trait Empty extends State; sealed trait Ready extends State

case class Request[S <: State](url: Option[String] = None):
  def withUrl(u: String): Request[Ready] = Request(Some(u))

extension (r: Request[Ready]) def send(): String = s"GET ${r.url.get}"

Request[Empty]().withUrl("http://x").send()
// Request[Empty]().send()     // compile error