And I think that is why the article, while in general touches a very interesting topic, leaves a bad aftertaste for me.
It makes it sound as if one method is better than the other - almost like "union types take away your power, better do it like this". However, both approaches have their place.
> As a more sensible example, I prefer Haskell's `Maybe` type compared to the "nullable types" offered by some languages, since the latter loses all structure
And you just gave a very good example. The two are not the same by any means and I am happy to be able to work in a language that has a Maybe type in the standard library. The classic example is having a map where querying by a key might or might not return a value of type A. Because A can also be null/none, without a Maybe type, this cannot be properly expressed without losing information.
However, and I really want to emphasize this: union types _can_ emulate sum types like Maybe. Simply be creating a wrapper and using it. I.e. creating two unrelated classes/structs Just and None and then defining
struct Just A = ...
struct None = ...
type Maybe A = Just A | None
There is Maybe, emulated by union types.The other way around is not possible though. Using Haskell#s capabilities, it is not possible to emulate Typescript's (or other language's) union types, which I find very sad. They are a useful thing to have. The only thing that might come close is Haskell Liquid and some fancy typelevel programming machinery to emulate union types.
Thank you, that makes sense. My impression was that typescript was chosen as an arbitrary language and that this limitation would be irrelevant for the example, but maybe it was chosen specifically.
I'm mainly a Scala developer, and in Scala 3 we can pretty much do:
case object A
case object B
type X = A | B
or type X = "A" | "B"
Where A and B didn't exist before, but "A" and "B" do. Does that capture the difference that the author wants to express?There is still a difference to Haskell though, in that the "case object A" is its own type and a subtype of X.
The (arbitrary, 'philosophical') difference between Haskell's `data A = X | Y` and TypeScript's `type A = "X" | "Y"` is that the TypeScript values "X" and "Y" already exist (they're strings). Functions which produce and consume these values already exist: there are certainly many functions which act on `string`, and perhaps there are silly examples which produce such strings too, e.g. a 2D geometry library which uses them to represent axes.
> without having to use wrappers and dealing with the overhead that they bring
I think part of the author's argument is that including such wrappers in our values can sometimes be useful, to convey more information about the domain. As an extreme example, we can represent pretty much everything in number theory using `int`; e.g. the fundamental theorem of arithmetic tells us that `int` is equivalent to `prime[]` (and of course Euclid proved that `prime` is equivalent to `int`), but it's probably more useful to use the second representation, even though it introduces wrappers and overhead.
As a more sensible example, I prefer Haskell's `Maybe` type compared to the "nullable types" offered by some languages, since the latter loses all structure: `Just Nothing` represents a different failure case to `Nothing` (and we can easily collapse them using `join`), whilst `null` doesn't provide that distinction.