The current scheme is: (1) singleton types in unions are not allowed. (2) Therefore, when taking the lub, we widen every singleton type. Thats simple and consistent, but also restrictive.
Note that singleton types in unions can still slip through the cracks:
scala> def or[T](x: T | Int) = x
or: [T](x: T | Int)T | Int
scala or["foo"]("bar")
res13: String | Int = bar
It's probably not worth trying to disallow this case before we try to make singleton types in unions actually work.
I uncovered another valid -- I think -- use-case while experimentitng with enums, which doesn't involve singleton literal types.
This is intended to be a linked-list-like datastructure, but with alternating types, and it might make a good test case, but currently doesn't compile due to the restriction.
enum Fence[+T, +S] {
case End
case Post(value: T, next: Panel[T, S] | End.type)
case Panel(value: S, next: Post[T, S])
}
import Fence._
val fence = Post(1, Panel("two", Post(3, End)))
Would have been nice to do:
type Binary = 0 | 1
There are other ways to achieve this constraint, though.
Just a curiosity, the or["foo"]("bar") example doesn't compile anymore, but it is still possible to get unsoundness with literal types in a union.
object Get2As1 {
class OrIntroFn[T, U, TU >: T|U]{
type V = TU
def tuToV(): (TU => V) = p => p
}
class Or11X[X] extends OrIntroFn[1&1, X, (1&1|X)]{
def get2as11X:V = tuToV()(2)
}
class Or11Nothing extends Or11X[Nothing]
def get2as1:1 = new Or11Nothing().get2as11X
def main(a:Array[String]) = {
println(get2as1) // prints 2
val one:1 = get2as1
println(one) // prints 1
}
}
In TypeScript this isn't an issue:
type ABC = 'A' | 'B' | 'C'
type A2F = ABC | 'D' | 'E' | 'F'
foo(x: A2F) {...}
foo('F') // ok
foo('G') // fails
Union types combined with string literals are quite powerful, not to mention concise -- above approach is used all the time in TypeScript -- would be great if this restriction were lifted.
We can perhaps workaround the issue with enums, but that's cumbersome in comparison to union types + string literals, which, if supported, wouldn't necessarily require defining a separate type; i.e. could just define in-place:
def foo(x: "A" | "B" | "C") = ...
useful for one-off cases where you want type safety but not be bothered with introducing a separate type.
Fixed by https://github.com/lampepfl/dotty/pull/6299 :sparkles:
Most helpful comment
In TypeScript this isn't an issue:
Union types combined with string literals are quite powerful, not to mention concise -- above approach is used all the time in TypeScript -- would be great if this restriction were lifted.
We can perhaps workaround the issue with
enums, but that's cumbersome in comparison to union types + string literals, which, if supported, wouldn't necessarily require defining a separate type; i.e. could just define in-place:useful for one-off cases where you want type safety but not be bothered with introducing a separate type.