Union types

+1 to what @AhmedMourad said.
Now that we’re going away from exceptions for error handling towards result types I find myself wanting union types almost every day. sealed classes are great but have severe limitations due to type hierarchy.

Let’s say I have 10 API endpoints. Each has a different successful result and a different set of error results.
4 can return UserNotFound, 7 can return NotAllowed, 2 can return InvalidPassword, all can return InternalError, and so on.

So I either

  • create 10 sealed classes (one per endpoint) where each lists all possible outcomes of the endpoint. That means there are 10 InnternalError subclasses, 7 NotAllowed subclasses etc… That means I cannot perform a generic error handling over NotAllowed and erase the type from the result because they are distinct types. Or…
  • I wrap everything into a generic Result<Value> (contains Value or Error) where Error is a common base type. Even if that Error is a sealed type I’d have to handle every possible error for every API endpoint even if they can never happen.

Both approaches have significant drawbacks.

Checked exceptions solved that problem, even though they had other issues. Now in Kotlin we don’t have checked exceptions and go towards using exceptions only for programming errors. That means we’re left with no good alternative to model data where a function can return multiple different outcomes, some if which are shared with other the outcome of other functions.

That’s a major use case I have quite frequently.

Another use case is JavaScript interoperability with Kotlin/JS. That is notoriously difficult if you have to consume TypeScript definitions with plenty of unions.

Another use case is better interoperability with GraphQL which does support union types.


Regarding JVM representation I think the “common supertype” approach is more than sufficient. The logic is already there in the compiler.

val foo = if (true) 1 else "bar" // = common supertype `Any`
// actually of common supertype `Comparable<*> & java.io.Serializable`

val bar = if (true) listOf(1) else setOf(2.2) // = common supertype `Collection<Any>`
// actually of common supertype `Collection<Comparable<*> & Number>`

I also wouldn’t mind if the Java API to a Kotlin library becomes “less safe” that way. If I want Java-interop and safety I’d not use unions. If I’m 100% in the Kotlin universe I don’t have to care about that.


Regarding overload conflicts on JVM I think that’s a non-issue.

We already have cases today where there is a legit overload in Kotlin (e.g. overloading by nullability or overloading by more specific generic type arguments) but it’s conflicting in JVM. Those cases are easily solved with the @JvmName annotation. The only exception are constructors which don’t allow for renaming.


Adding and removing types from a union (similar to @sollecitom’s example) would be an important feature. Also something the compiler is already capable of. But the syntax would be tricky.

fun <T> handleB(value: T|B): T without B
   = …

Roughly like this.

Use case:

fun <Result> process(result: Result|Error, processor: (Result without Error) -> Unit) {
   if (result is Error)
      // handle error
   else
     processor(result)
}

Some syntax thoughts:

  • Any type that’s not A~A
  • A type that’s T or AT | A
  • A type that’s T and AT & A
  • A type that’s T but not AT & ~A (the without from above)

So if T = A | B | C then T & ~A = B | C.
Basically like binary operators, but for types.

8 Likes