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 theInt
andString
cases in the match expression. (In Scala 2 you would use the typeAny
, and in Scala 3 you would use the typeMatchable
.)
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.
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |
See Also
- a href="https://docs.scala-lang.org/sips/42.type.html">The SIP for Literal-Based Singleton Types
- My free “Scala 3 Union Types” video