3 ms·
Union types are not the same as sum types.
by fanf2 3y ago
Union types are not the same as sum types.
- dtech 3y agoTS narrows union types cases based on conditionals like "if" (called discriminated unions in the docs in the past), and supports exhaustiveness checks. How do they differ in functionality from sum types?
- mpawelski 3y agoSupports exhaustiveness checks only if you explicitly opt-in it (by coding to pattern where you use helper function that accepts `never` type). "Dicriminated Unions Type"/"Sum Types" feels very hacky there, at least syntax-wise, because it is constraint by being "JS + types" language. It's remarkable what Typescript can do, but having native Discriminated Unions in JS (hence in TS too) would be much more ergonomic and powerful.
- nyssos 3y agoSum types are disjoint unions. This `T` has three cases L = { tag: "a", payload: string } | { tag: "b", payload: number } R = { tag: "b", payload: number } | { tag: "c", payload: boolean } T = L | R whereas a proper sum type `L + R` would have four.
- brabel 3y agoIsn't that a completely useless distinction? For all purposes and intents, the "b" type in L and R should be treated the same, no? What do you gain by not doing that??
- hexane360 3y agoThis often comes up when writing a function which returns a wrapper over a generic type (like Option<T>). If your Option type is T | null, then there's no way to distinguish between a null returned by the function or a null that is part of T. As a concrete example, consider a map with a method get(key: K) -> Option<V>. How do you tell the difference between a missing key and a key which contains `null` as a value?
- brabel 3y agoThis is trivial to model by making your type `T | null | Missing`.
- efnx 3y agoMaybe trivial to “work around” but there is a difference, ay? With this type you would have to check/match an extra case! The type you use there also takes more memory than Option<T> or Maybe<T>. So it has some other downsides.
- epolanski 3y agoOr just using Option since you would have Some<null> or None in that case.
- hexane360 3y agoOnly if you're designing both functions ahead of time. In other words, it's not composable.
- epolanski 3y ago`T | null` is equivalent to T. You can assign null to `T`> It's like saying `string | "foo"` it is simply `string` due to subtyping.
- hexane360 3y ago... no? Unless you're referring to null as a bottom type, then that doesn't hold. Are you describing some property of a specific language?
- Twisol 3y agoNo, it isn't "completely useless". If you have a function that will normally return a string, but can sometimes fail due to reasons, you may wish to yield an error message in the latter case. So you're going to be returning a string, or a string. It's not what the content of the data is; it's how you're supposed to interpret it. You have two cases, success and failure, and control will flow differently depending on that, not based strictly on the type of data at hand. We just model those cases in a type.
- brabel 3y agoNo disrespect, but that still sounds entirely useless to me. I would never model something as `String | String` as that makes zero sense. You should use a `Result` or `Either` type for that like everyone does.
- Twisol 2y ago> You should use a `Result` or `Either` type for that like everyone does. You have missed the thread context, which is whether `Either a a` (also written `a + a`) has any merits over simply `a` (which is identical to `a | a`). If you're on the `Either` train already, we are arguing over imaginary beef. > No disrespect, but that still sounds entirely useless to me. It is disrespectful to say something "makes zero sense", regardless of anything you might say to the contrary. You've misrepresented my point: nobody wants to model something as `String | String`. If you have, say, an `Int | Bool`, and you pass both sides through some kind of stringification function, you're naturally going to get `String | String` without ever having written that type down yourself. You wouldn't necessarily want to collapse that to `String`, however, because you may -- for instance -- want to give strings and ints different decorations around them before finally flattening them. You might write such a function as something like (ib) => ib .mapBoth(showInt, showBool) .visit(prepend("int: "), prepend("bool: ")); You couldn't run this if the result of the `mapBoth` had type `String | String`: that type is indistinguishable from `String`, and since you can't tell which case you're in, you wouldn't know which tag to prepend. You could write the same function without passing through `String | String`: (ib) => ib.visit( sequence(showInt, prepend("int: ")), sequence(showBool, prepend("bool: "))); And yes, in this especially contrived example, perhaps you may find that to make more sense anyway. But in longer pipelines, sometimes separated over multiple functions, often involving generic code, it becomes much harder to simply always fuse steps in this manner. This is why we say sum types (e.g. `String + String`) compose better: they don't behave any differently depending on whether the two sides are the same or not. You have explicit control over when the two sides rejoin.
- akavi 3y agoAs you've demonstrated, you can always construct sum types in typescript with the use of explicit discriminants: T = {tag: "L", payload: L} | {tag: "R", payload: R} The real issue is typescript doesn't have pattern-matching, which make operating on these sum types inelegant