I don’t agree with the article’s claim that discriminated unions are just a JS compat feature. Many APIs type JSON serialised entities using a string property and this feature makes it very easy to write interfaces and code to work with them.
also bear in mind that support for discriminated unions is what made typescript work nicely with redux actions - that is be able to strongly type actions/reducers/dispatch functions.
One thing I love about typescript is they care more for the actual JS/library ecosystem as-is, and don't design a language for an ecosystem that should-be.
FWIW, I'm a Redux maintainer, and I personally have never understood the point of trying to limit what actions are being dispatched. As long as your reducers and action creators are well typed, it shouldn't matter what other actions might be sent through.
On that note, our new Redux Toolkit package is written in TS, and designed to work great in TS apps with a minimal amount of type declarations needed. Really just declare the type of the action in the reducer, and get everything else for free. The new React-Redux hooks API is also a lot easier to use with TS as well.
I showed how to use both of those in the Redux Toolkit "Advanced Tutorial" docs page:
On a related note, I also recently put up a long blog post detailing my own journey learning and using TS, as both a lib maintainer and an app developer, with my takeaways on the pros and cons of using TS:
> I personally have never understood the point of trying to limit what actions are being dispatched. As long as your reducers and action creators are well typed, it shouldn't matter what other actions might be sent through.
The value comes when you consider what happens if a reducer (or saga or whatever) is updated in a breaking way (or removed wholesale).
Example: consider a scenario where you use `connected-react-router`, your codebase fills up with history actions being dispatched, then one day you remove `connected-react-router`, or its major version is updated and introduces some breaking change.
If you have a union type that includes all your own actions plus the `LocationChangeAction` from connected-react-router (and you use is consistently - e.g. your components take `Dispatch<YourAction>`) then you'll learn about the breaking change when your app fails to compile.
The alternative is you catch it later than compile-time, at worst _much_ later.
You might decide that the overhead in this scenario isn't worth it, and that's your call to make (I believe it is worth it) but hopefully this gives an idea of the point.
Personally I always found it annoying to manually add a common tag to every interface/type on TS just to have it work like an union type. But since TS doesn't leak types to runtime that's the only way.
Author here! Yeah I think you are right that discriminated unions are useful more broadly than just legacy JS code.
That being said, I still think TypeScript's solution to handling them of using "type guards" where the type of a variable changes in different scopes is definitely designed to match common JS patterns at the cost of added complexity. Most other languages I know of only have a single type for a variable (and if you want to pattern match you must give each case a new variable name).
That’s control-flow sensitive type deduction though - not specific to union types. I agree that proper patten matching is much nicer - but unavailable in JS.
> Most other languages I know of only have a single type for a variable
This may be pedantic, but with subtyping many OO languages can give many different distinct types for a variable. In Java you can assign any non-primitive typed expression to a variable of type Object. So almost any expression in Java can be typed as Object.
What you are describing as novel is rather the phenomenon of type refinement in a pattern match or conditional expression. When an expression undergoes pattern matching, its type becomes increasingly refined. This is a useful feature in intermediate-to-advanced Haskell (known as GADT) as well as dependently typed languages.
> In Java you can assign any non-primitive typed expression to a variable of type Object.
Sure, but if you do this in Java you must use different variable names for the reference of type Animal and the reference of type Object.
I think algebraic data types and type refinement are great, but I’m not a fan of automagically changing the types of variables in different scopes if it can’t be applied consistently.
Hacklang also has something like type guards called type refinement [0].
I'm still kinda new to the idea of using conditional branches to inform static type checkers but it sounds like it's an idea that has been thought about in some depth [1][2][3] (i.e. doesn't sound like it was jimmy-rigged to match common JS patterns).
I personally love type refinement. Hacklang's type refinement was the first time it clicked that statically typed languages could _actually help_ you write code instead of get in the way.
> Most other languages I know of only have a single type for a variable (and if you want to pattern match you must give each case a new variable name).
To add to what others have said: Kotlin is a language that does path sensitive (re-)typing of variables. This is often very convenient in the presence of OO-style polymorphism or in cases where the language supports ad-hoc unions.
Personally it doesn't feel complicated to use/understand (at least the kind of thing that these languages do)
I don't quite follow. Discriminated unions allow you to derive the type from the runtime validation. If the runtime finds these properties then it must be one of these, but if it finds those then it's one of those. If it can't fit into one of those type buckets as defined by runtime validation, then its <unknown>. The types flow from the validation algebraically.
I'm assuming you don't understand why you shouldn't use discriminated unions for server-side validation?
Client (JSON) messages will pretty much always start out as any, and you can't assume they fit your union any way shape or form. Taking the Cat|Dog example from the article, a client may pass {"kind":"cat", "bark": "woof"} and if you just go blindly from there to the union and then try to narrow that down, you've got yourself a problem.
You should use a library like JOI (or type guards with custom validation) to first make sure it fits your union type, then you can go wild with discriminated unions.
I always wanted a library that could perform runtime validation given a typescript type, but so far typescript doesn't expose enough type information in decorators for this.
But wouldn't what you're talking about just be a union type? We might be saying the same thing. The "discriminated" part of discriminated union means that you will be performing the runtime validation to check which of Cat or Dog it is, and only once it receives those validations do you have a derived type. By asserting that the object received is Dog only if it has kind:dog bark:string, you've narrowed your type at compile time to only types that have bark.
> But wouldn't what you're talking about just be a union type?
Not really, unless you define any as union of every possible type. Also TypeScript won't realize you have narrowed the type (because it doesn't use such a definition) with all your checking unless you cheat with type guards (which are really just a prettier cast with optional user-created validation).
Then there's also the fact that thanks to getters your narrowing from any may be incorrect, which is probably why you can only narrow to basic types or with an instanceof operator, neither of which will help you with your JSON input.
tl;dr: Discriminated unions won't help you when you're starting out from any, because any is not an union.
True, discriminated unions don't flow from any, but they can be useful for composing types from any.
I suppose I'm saying that the validation function itself is the precursor to discriminated unions being useful. <any> run through a type predicate function or an assertion type function (https://www.typescriptlang.org/docs/handbook/release-notes/t...) produces a typed object that conforms to your requirements for the type.
I definitely do wish there were some way to create these assertions/validators from the types automatically, but I think that might be impossible.
function isAnimal(thing: any): asserts thing is Animal {
if (thing.kind !== "dog" && thing.kind !== "cat && etc){
throw new AssertionError("Not an animal!");
}
```
// ...etc, except that string literals probably exist in an Array or similar, ie types that can be progressively derived / enhanced from the runtime code.