But for me the main use for TS union types is to make discriminated unions, which is very useful wherever you have some form of a state-machine:
type AppState =
| { state: "loading", progress: number }
| { state: "selecting_level", }
| { state: "playing", level: Level }
| { state: "success" }
| { state: "failure", wantsRetry: boolean }
| { state: "error", reason: string }
(Small excerpt from a game we're developing at $dayjob)You can switch on that and the compiler will know what variant you're talking about, and it will apply exhaustiveness checks (ensures your code will handle all possible states)
abstract class AppState {}
class AppLoading extends AppState {
final int progress;
}
class AppSelectingLevel extends AppState {}
class AppPlaying extends AppState {
final Level level;
}
...
Certainly not as neat, but also, not that far off either. Then where you use this app state, you could switch over its `runtimeType`, and in each clause, the IDE will understand which implementation you are dealing with, and actually give you context sensitive help related to that particular state.If you instead only cared about one case, you would be able to do:
if (appState is AppPlaying) {
doSomething(appState.level);
}
Would this be somewhat analogous to the functionality you get with unions in typescript?The TS code indeed looks cool. This is an area we're looking at.
One point, though: we try to be very careful to not regress performance or developer iteration time (e.g., type checking time) when we introduce new language features. E.g., structural typing can be more expensive in general to type check since we need to recurse.
Have you considered not going full-on structural-typing but still providing some sort of union? In fact, you could go for one with even stronger guarantees, like the sum types in Rust or F#. (with Rust going as far as to call them enums too)
I'll admit I have the faintest notion on what causes that kind of complexity on a compiler, so my suggestion might be an even worse idea.
I work on Dart. The terminology gets really confusing here. "Discriminated unions" and "union types" are two quite different things, though they get a little blurry in TS.
The short answer is, yes, we're investigating pattern matching with exhaustiveness checking and making the language more graceful at expressing algebraic datatype-style code. The last half of that sentence sounds weasely because any object-oriented language can (except for exhaustiveness checking) model an algebraic datatype using subtyping. The parent comment using an abstract base class is how you do it.
So there isn't anything really fundamental that Dart can't express already. It's mostly a question of giving users syntactic sugar to make code in that style look more terse and familiar. I personally really like pattern matching and ADTs and I also particularly like multi-paradigm languages, so this is a subject close to my heart.
The language team has been pretty busy with null safety, but now that that's out the door (woo!), we can start working on the next batch of features, which with luck includes pattern matching. Here's an in-progress proposal:
https://github.com/dart-lang/language/blob/master/working/05...
Unless I'm mistaken, if one were to later implement a new class that extends AppState, all existing code would compile, but possibly fail or misbehave at runtime, unless you meticulously checked every place that tries to determine something based on those derived types.
In TypeScript, adding a new case for an union and not handling it everywhere is a compilation error on every incomplete usage site.
For example, try deleting one of the arms of the switch in this playground: https://www.typescriptlang.org/play?ts=4.2.2#code/C4TwDgpgBA...
I have to say, the default diagnostic isn't brilliant, but some tooling will give a better error and actually point out the missing arms, instead of complaining about the return type.
All in all, sufficiently analogous to not consider unions a missing feature of the dart language? Seems nice to have, but, maybe not very necessary. Especially if the only difference is whether it is considered an error by the syntax, or a warning by the linter.
Scala 2 also had `sealed` classes that could be used in places where you needed enums parameterized by runtime values and that's been generalized in Scala 3 IIRC.
But, in practice, a language's type system tends to optimize for the data structures that are idiomatic in the language. TypeScript is a heavily structural type system because idiomatic JavaScript often throws together unrelated types in this way (which makes sense when you're coming from dynamic types).
In a language that is built more strongly on objects and static types, it's less common to see APIs that "return one of these, or one of these, or on Sunday one of these things". So there is relatively less use for union types.
In other words, because people express unions at runtime less frequently in Dart (and other languages like Java and C#), there's less value in supporting them statically. Still some, definitely, but I don't think it's as critical of an omission as you might expect coming from TS/JS.
The "people use fewer unions in Java" argument is like saying people used fewer lambdas in Java 1.6. Build it, and they will use it.
I think you're confusing union types and sum types, which are very distinct features. SML, Haskell, and Rust don't have union types.
OOP languages can model sum types already. It's mostly a question of how much the surface syntax encourages that style.
I think _closed_ union types (the syntax `a | b | c`) are almost as good as sum types, since they bring exhaustiveness checking to the mix. They also act as documentation. Crystal is a good example of a language leveraging this feature.
The OO model of just subclassing a common class is quite poor by itself. Scala and Kotlin have a 'sealed' modifier to at least recover the exhaustiveness check. Both also have ways of safely doing the case switch on the object's class and simultaneously downcasting (Java too with its recent `match` construct, afaik). Does Dart have anything like that?
The natural OOP way to model an operation that must exhaustively be supported by a set of types is by making it an abstract method on a shared superclass. For problems where it's natural to keep the operations textually near each type, that works great.
Otherwise, yes, you'd ideally have some notion of a sealed family of types so that you can do case analysis with exhaustiveness checking. That lets you ensure all operations are covered when you program either in OOP or FP style.
Something I find odd is that FP folks often criticize OOP languages for not supporting exhaustiveness checking, but I rarely hear them admit that most FP languages don't have support for the other style that OOP does well, which is abstract methods.
- Scala has the whole OOP stuff - F# as well - SML: you probably need to do it by hand, with a record of closures, or something like that. It's ugly but it works. - OCaml has objects (it's in the name), first-class modules, or records-of-functions; all of which can replace abstract methods successfully. - Haskell can rely on typeclasses to do the virtual dispatch, I think. Not an expert on that.
So I'd say it might not be as ergonomic, but the capability is still 100% there.
my_type = my_list | my_object
?
You can see why this is better :-)
Dart still does not even have union types, probably the best feature of TS. I would hold off on that claim for now..