I personally am not a huge fan of @nickallendev proposal.
Creating a new instance every time a function with a union is called is not a good thing for performance.
Here is my own proposal:
A. Definition of Union type
A union type can be defined as [TypeA | TypeB | TypeC], which allows (non exhaustive):
- argument type definition:
fun toJson(value: [String | Number])
- receiver type definition:
fun [String | Number].toJson()
- return type definition:
fun nextElement(): [String | Number]
- variable / value type:
val element: [String | Number]
And most importantly:
- type alias definition:
typealias Element = [String | Number]
B. Compilation
A union type is compiled as the first super common type, annotated with @KotlinUnion.
[String | Number] is compiled as @KotlinUnion(String::class, Number::class) Any
[String | StringBuilder | StringBuffer] is compiled as @KotlinUnion(String::class, StringBuilder::class, StringBuffer::class) CharSequence
C. Direct usage
Without being disambiguated, a union value can only be used as a union type that is equivalent or larger (e.g. a [String | Number] value can be used as a [String | Number | Boolean]).
D. Disambiguation
A union value can be disambiguated either via a when or a if statement:
fun toJson(value: [String | Number | Boolean]): String {
return when (value) {
is String -> "\"" + value.jsonEscaped() + "\""
is Number -> value.toString()
is Boolean -> if (value) "true" else "false"
}
}
Notice the absence of else clause.
E. Generics parameters & wrapping
Union types cannot be used as generic parameter (e.g. List<[String | Number]> is forbidden). However, union types can be used in inline classes, making them usable as generic parameter.
Therefore, this is legal:
inline class Element(val element: [String | Number])
val list = ArrayList<Element>()
F. Generic components
Union types cannot contain generic components (e.g. fun <T> foo(v: [String | T]) is forbidden).
However, it is possible for a union type to contain an inline reified generic type.
Therefore, these are legal:
inline fun <reified T> foo(v: [String | T])
inline fun <reified T> Map<String, T>.remove(v: [String | T])
G. Java compatibility
In non-private methods & functions (e.g. java accessible), much like the non-null check intrinsic, a type check is added at the start of the function:
if (v !is String && v !is Number) throw IllegalArgumentException("$v does not conform to [String | Number]")
Note that the performance cost of instanceof is known to be very small.