Scala 3 — Book

Union Types

Language
This doc page is specific to Scala 3, and may cover new concepts not available in Scala 2. Unless otherwise stated, all the code examples in this page assume you are using Scala 3.

Used on types, the | operator creates a so-called union type. The type A | B represents values that are either of the type A or of the type B.

In the following example, the help method accepts a parameter named id of the union type Username | Password, that can be either a Username or a Password:

case class Username(name: String)
case class Password(hash: Hash)

def help(id: Username | Password) =
  val user = id match
    case Username(name) => lookupName(name)
    case Password(hash) => lookupPassword(hash)
  // more code here ...

We implement the method help by distinguishing between the two alternatives using pattern matching.

This code is a flexible and type-safe solution. If you attempt to pass in a type other than a Username or Password, the compiler flags it as an error:

help("hi")   // error: Found: ("hi" : String)
             //        Required: Username | Password

You’ll also get an error if you attempt to add a case to the match expression that doesn’t match the Username or Password types:

case 1.0 => ???   // ERROR: this line won’t compile

Alternative to Union Types

As shown, union types can be used to represent alternatives of several different types, without requiring those types to be part of a custom-crafted class hierarchy, or requiring explicit wrapping.

Pre-planning the Class Hierarchy

Without union types, it would require pre-planning of the class hierarchy, like the following example illustrates:

trait UsernameOrPassword
case class Username(name: String) extends UsernameOrPassword
case class Password(hash: Hash) extends UsernameOrPassword
def help(id: UsernameOrPassword) = ...

Pre-planning does not scale very well since, for example, requirements of API users might not be foreseeable. Additionally, cluttering the type hierarchy with marker traits like UsernameOrPassword also makes the code more difficult to read.

Tagged Unions

Another alternative is to define a separate enumeration type like:

enum UsernameOrPassword:
  case IsUsername(u: Username)
  case IsPassword(p: Password)

The enumeration UsernameOrPassword represents a tagged union of Username and Password. However, this way of modeling the union requires explicit wrapping and unwrapping and, for instance, Username is not a subtype of UsernameOrPassword.

Inference of Union Types

The compiler assigns a union type to an expression only if such a type is explicitly given. For instance, given these values:

val name = Username("Eve")     // name: Username = Username(Eve)
val password = Password(123)   // password: Password = Password(123)

This REPL example shows how a union type can be used when binding a variable to the result of an if/else expression:

scala> val a = if true then name else password
val a: Object = Username(Eve)

scala> val b: Password | Username = if true then name else password
val b: Password | Username = Username(Eve)

The type of a is Object, which is a supertype of Username and Password, but not the least supertype, Password | Username. If you want the least supertype you have to give it explicitly, as is done for b.

Union types are duals of intersection types. And like & with intersection types, | is also commutative: A | B is the same type as B | A.

Contributors to this page: