Scala 3 Unions: Simulating Dynamic Typing with Union Types

This is an excerpt from the Scala Cookbook, 2nd Edition. This is Recipe 23.9, Simulating Dynamic Typing with Union Types.

Problem

When using Scala 3, you have a situation where it would be helpful if a value could represent one of several different types, without requiring those types to be part of a class hierarchy. Because the types aren’t part of a class hierarchy, you’re essentially declaring them in a dynamic way, even though Scala is a statically-typed language.

Solution

In Scala 3, a union type is a value that can be one of several different types. Union types can be used in several ways.

In one use, union types let us write functions where a parameter can potentially be one of several different types. For example, this function, which implements the Perl definition of true and false, takes a parameter that can be either an Int or a String:

// Perl version of "true"
def isTrue(a: Int | String): Boolean = a match
    case 0  => false
    case "0" => false
    case "" => false
    case _ => true

Even though Int and String don’t share any direct parent types — at least not until you go up the class hierarchy to Matchable and Any — this is a type-safe solution. The compiler is smart enough to know that if I attempt to add a case that tests the parameter against a Double, it will be flagged as an error:

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

In this example, stating that the parameter a is either an Int or a String is a way of dynamic typing in a statically-typed language. If you wanted to match additional types you could just list them all, even if they don’t share a common type hierarchy (besides Matchable and Any):

class Person
class Planet
class BeachBall

// the type parameter:
a: Int | String | Person | Planet | BeachBall

Note: The only way to write that function prior to Scala 3 was to make the function parameter have the type Any, and then match the Int and String cases in the match expression. (In Scala 2 you would use the type Any, and in Scala 3 you would use the type Matchable.)

In other uses, a function can return a union type, and a variable can be a union type. For example, this function returns a union type:

def aFunction(): Int | String =
    val x = scala.util.Random.nextInt(100)
    if (x < 50) then x else s"string: $x"

You can then assign the result of that function to a variable:

val x = aFunction()
val x: Int | String = aFunction()

In either of those uses, x will have the type Int | String, and will contain an Int or String value.

Discussion

A union type is a value that can be one of several different types. As shown, it’s a way to create function parameters, function return values, and variables that can be one of many types, without requiring traditional forms of inheritance for that type. Union types provide an ad-hoc way of combining types.

Combining union types with literal types

In another use, you can combine union types with literal types to create code like this:

// create a union type from two literal types
type Bool = "True" | "False"

// a function to use the union type
def handle(b: Bool): Unit = b match
    case "True"  => println("true")
    case "False" => println("false")

handle("True")
handle("False")
handle("Fudge")  // error, won’t compile

// this also works
val t: Bool = "True"
val f: Bool = "False"
val x: Bool = "Fudge"  // error, won’t compile

The ability to create your own types using the combined power of literal types and union types gives you more flexibility to craft your own APIs.

See Also