TL;DR
- Union Types (
|): One of several types,Int | String- Intersection Types (
&): Satisfies all types,A & B- Opaque Types: Type safety without runtime overhead
- Match Types: Type-level pattern matching
- Scala 3-only features for more powerful type expression
Target Audience: Scala developers who understand generics and variance Prerequisites: Type parameters, type bounds, variance
Scala 3 provides an even more powerful and expressive type system. This document covers Scala 3’s new type features.
Prerequisites: To understand this document, you should be familiar with:
- Generics - Type parameters, type bounds
- Variance - Variance concepts
- Pattern Matching - Basic pattern matching
Difficulty: ⭐⭐⭐⭐ (Advanced) - Includes Scala 3-only features
Union Types (|)#
Union Types represent one of several types. You can work directly with values without wrappers, making them more concise than Either.
// Scala 3 only
def process(input: Int | String): String = input match
case i: Int => s"Number: $i"
case s: String => s"String: $s"
process(42) // "Number: 42"
process("hello") // "String: hello"
// Multiple types
type JsonValue = String | Int | Boolean | Null
def toJson(value: JsonValue): String = value match
case s: String => s"\"$s\""
case i: Int => i.toString
case b: Boolean => b.toString
case null => "null"Comparison with Either
// Either: Requires explicit Left/Right wrapper
def divideEither(a: Int, b: Int): Either[String, Int] =
if b == 0 then Left("Cannot divide by zero") else Right(a / b)
// Union: Return value directly without wrapper
def divideUnion(a: Int, b: Int): Int | String =
if b == 0 then "Cannot divide by zero" else a / bKey Points
- Union Type represents one of several types in form
A | B- Unlike Either, works directly with values without wrappers
- Use pattern matching to distinguish and handle types
Intersection Types (&)#
Intersection Types satisfy multiple types simultaneously. Useful when an object must implement multiple traits at once.
trait Printable:
def print(): String
trait Serializable:
def serialize(): Array[Byte]
// Type that is both Printable and Serializable
def process(obj: Printable & Serializable): Unit =
println(obj.print())
val bytes = obj.serialize()
// Implementation
class Document(content: String) extends Printable, Serializable:
def print(): String = content
def serialize(): Array[Byte] = content.getBytes
process(Document("Hello"))Structural Type Combination
type Named = { def name: String }
type Aged = { def age: Int }
def describe(obj: Named & Aged): String =
s"${obj.name}, ${obj.age} years old"Key Points
- Intersection Type satisfies all types in form
A & B- Used when an object must implement multiple traits simultaneously
- Can be combined with structural types for flexible type definitions
Opaque Types#
Opaque Types are type aliases that are treated as different types externally. They provide type safety without runtime overhead, very useful for domain modeling.
object UserId:
opaque type UserId = Long
def apply(id: Long): UserId = id
extension (id: UserId)
def value: Long = id
def isValid: Boolean = id > 0
import UserId.*
val id: UserId = UserId(42)
id.value // 42
id.isValid // true
// val x: Long = id // Compile error! UserId != LongBenefits
- No runtime overhead (no boxing)
- Provides type safety
- Useful for domain modeling
object Money:
opaque type USD = BigDecimal
opaque type EUR = BigDecimal
def usd(amount: BigDecimal): USD = amount
def eur(amount: BigDecimal): EUR = amount
extension (x: USD)
def +(y: USD): USD = x + y
def value: BigDecimal = x
import Money.*
val dollars = usd(100)
val more = usd(50)
val total = dollars + more // OK: USD + USD
// val mixed = dollars + eur(50) // Compile error! USD + EURKey Points
- Opaque Type is a type alias treated as different type externally
- Provides type safety without runtime overhead (no boxing)
- Useful for preventing type confusion in domain modeling
Match Types#
Match Types perform pattern matching at the type level. They can compute different result types based on input types.
type Elem[X] = X match
case String => Char
case Array[t] => t
case Iterable[t] => t
val a: Elem[String] = 'a' // Char
val b: Elem[Array[Int]] = 1 // Int
val c: Elem[List[String]] = "hi" // StringRecursive Match Types
type Flatten[X] = X match
case List[List[t]] => Flatten[List[t]]
case List[t] => List[t]
// List[List[List[Int]]] -> List[Int]
val x: Flatten[List[List[List[Int]]]] = List(1, 2, 3)Key Points
- Match Type performs pattern matching at type level
- Computes different result types based on input types
- Complex type transformations possible with recursive Match Types
Type Lambdas#
Type Lambdas allow you to pass type constructors as type parameters. Useful when working with higher-kinded types.
// Type lambda: [X] =>> F[X, Y]
type IntMap = [V] =>> Map[Int, V]
val map: IntMap[String] = Map(1 -> "one", 2 -> "two")
// Actually Map[Int, String]
// Either with fixed error type
type Result = [A] =>> Either[String, A]
val ok: Result[Int] = Right(42)
val err: Result[Int] = Left("Error")Key Points
- Type Lambda expresses type constructors in form
[X] =>> F[X]- Useful for partial application of type parameters when working with higher-kinded types
- Provides a concise alternative to Scala 2’s complex syntax
Dependent Function Types#
Dependent Function Types are function types where the return type depends on the parameter value.
trait Key:
type Value
val intKey = new Key { type Value = Int }
val strKey = new Key { type Value = String }
// Return type depends on key's Value type
def get(key: Key): key.Value = ???
// Dependent function type
val getter: (key: Key) => key.Value = (key: Key) => ???Polymorphic Function Types#
Polymorphic Function Types are function types with type parameters. Function values themselves can be generic.
// Polymorphic function type
val identity: [A] => A => A = [A] => (a: A) => a
identity[Int](42) // 42
identity[String]("hi") // "hi"
// First element of list
val head: [A] => List[A] => A = [A] => (list: List[A]) => list.head
head(List(1, 2, 3)) // 1Comparison with Scala 2#
The table below summarizes differences in advanced type features between Scala 2 and Scala 3.
| Feature | Scala 2 | Scala 3 |
|---|---|---|
| Union Types | Use Either | A | B |
| Intersection | A with B | A & B |
| Opaque Types | Value Class | opaque type |
| Match Types | Not possible | Supported |
| Type Lambdas | Complex syntax | [X] =>> F[X] |
Exercises#
1. Implement Opaque Type ⭐⭐
Implement an Email opaque type. Include validation.
Show Answer
object Email:
opaque type Email = String
def apply(value: String): Option[Email] =
if value.contains("@") && value.contains(".") then Some(value)
else None
def unsafe(value: String): Email = value
extension (email: Email)
def value: String = email
def domain: String = email.split("@").last
import Email.*
val valid = Email("user@example.com") // Some(Email)
val invalid = Email("invalid") // None
valid.foreach(e => println(e.domain)) // "example.com"Next Steps#
- Macros — Compile-time code generation
- Functional Patterns — Functor, Monad