Nitpicky but critical detail: union types are not the same as "sum" types such as what you get in Reason, Purescript, and Elm. Union types are convenient for adding atop a dynamic language or modeling records (although, Purescript's row types are better), but they are less capable of abstraction.
The key example to differentiate union and sum types is `int union int == int` whereas `int sum int` is something unique and different from `int`. A sum type keeps track of the "left side" versus the "right side".
So, all said and done not only is Reason producing faster, tinier code... it's also managing a more sophisticated and powerful abstraction!
Sum types are not more capable of an abstraction than union types. If we consider them in isolation, they are strictly less capable. For example, its possible to model sum types using union types in TypeScript by simply adding a tag field:
type Either<T, U> = { kind: 'left', left: T }
| { kind: 'right', right: U }
If you do this, typescript will also be able to do exhaustiveness checks provided its configured with noImplicitAny and strictNullChecks:
For example, try removing a case item in the below code (paste it on http://www.typescriptlang.org/play/ and in options turn on the above mentioned checks):
type Stringifiable = { toString(): string };
function stringify<T extends Stringifiable, U extends Stringifiable>(thing: Either<T, U>):string {
switch (thing.kind) {
case 'left': return 'Left ' + thing.left.toString()
case 'right': return 'Right ' + thing.right.toString()
}
}
edit: additionally, PureScript row types are not strictly better. While row polymorphic records are a better tool to model records (they can model open or closed records and subtype relations are not needlessly complex), AFAIK PureScript still lacks advanced transformation tools such as mapped and conditional types which really make TS records shine (see https://www.typescriptlang.org/docs/handbook/release-notes/t... for example)
Motivating example (given a record type, returns a record type containing only the properties that are not functions):
type NonFunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? never : K
}[keyof T];
type NonFunctionProperties<T> = { [P in NonFunctionPropertyNames<T>]: T[P] };
You can encode sums using unions as you note, but you need to do this in order to have good abstraction. For instance, an option type `type Option<T> = null | T` has the weird property of being idempotent `Option<Option<T>> == Option<T>` which means that operations working generically over `Option` cannot exploit free theorems over T.
Mapped and conditional types add power to Typescript's repertoire, surely. On the other hand, this isn't a deficiency of _row_ types but instead the total position of Purescript's type system as opposed to Typescript. I just posit that row types are a nicer formulation for handling records than union/intersection atop sets of key mappings.
Depends on what you mean by "good abstraction". If I wish to use sum types to represent discriminated unions and parametric polymorphic types then yes its better to make sure the type "sets" don't intersect.
I wouldn't really consider this encoding special though - its standard practice e.g see Redux. The usage syntax could be better, but its very much a native, commonly accepted practice that works well.
If however I want to use unions for other things, like simple unions of `string | number` I can do that too. For example, I can also describe overlapping records of "options" where specifying one option means you have to specify another, and I can model that without nesting the option subsets. I can't do that with sum types.
Its unfortunate but lots of JS libraries do mix parametric types with their parameters e.g. `T | Promise<T>` means that Promise<Promise<T>> is the same as Promise<T>, however at least we can model that in the type system.
TypeScript is worse at all things involving parametric polymorphism, thats true. Mainly due to lack of higher kinded types, but also due to the bad modeling habits of parametric things built into the JS language (not just promises - look at array concat). But thats why I qualified my statement with "if you look at sum types in isolation"
With records, though, its just way ahead - so much so that I'm bothered by the primitive built in records in most other languages. Its great that row polymorphism is a nicer way to handle records, but I need the power too, and TS gives me that.
I'm not arguing that unions aren't a good choice for Typescript with its given aim to stay close to normal JS semantics. These choices make it difficult to abstract in raw JS as well (see all the Promises-aren't-monads noise).
And yeah, type-level programming is super powerful. You could install it into PS if you wanted, but I don't think that's the goal there.
The key example to differentiate union and sum types is `int union int == int` whereas `int sum int` is something unique and different from `int`. A sum type keeps track of the "left side" versus the "right side".
So, all said and done not only is Reason producing faster, tinier code... it's also managing a more sophisticated and powerful abstraction!