+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, 7NotAllowed
subclasses etc… That means I cannot perform a generic error handling overNotAllowed
and erase the type from the result because they are distinct types. Or… - I wrap everything into a generic
Result<Value>
(containsValue
orError
) whereError
is a common base type. Even if thatError
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
orA
—T | A
- A type that’s
T
andA
—T & A
- A type that’s
T
but notA
—T & ~A
(thewithout
from above)
So if T = A | B | C
then T & ~A = B | C
.
Basically like binary operators, but for types.