Snipped and backdated from a previous Reddit comment of mine.
One major annoyance this alleviates is strongly typed collections with mixed types.
Now, this shouldn’t be confused with regular algebraic data types (ADTs) a la Haskell or F#’s “discriminated unions”, for example. ADTs kind of solve the problem but in a less powerful way. You have to define a new ADT type upfront and you can’t “mix” ADTs togeather. You only can compose them.
For example, if I wanted to define a list that contains either an
string, or “nothing” in F#, I would do:
type Option<'a> = | Some of 'a | None type Element = | EInt of int | EString of string let xs: list<Option<Element>> = [Some(EInt 1); Some(EString "hello"); None]
In F#, the type inferencer would infer that the type of
xs without me writing the type out, but I had to still specify a new ADT upfront. And I can’t properly mix the types in the
Option ADT with the types in
Element ADT and have to wrap around the other.
Instead, in a type system with union and intersection types, you could make a union of the different types you want like:
let xs: list<int | string | null> = [1; "hello"; null]
And ideally, you would have type inference so you could have it still be statically typed without having to type out all possibilities:
let xs = [1; "hello world"; null];
You would then need to narrow the type down in order to safely use a value. This could be accomplished by using either pattern matching or by using control flow analysis “as a language construct” (Typescript has “type guards”, Kotlin has “smart casts”).
The funny similarity is that both do the same thing, but the former is usually implemented in a functional-style language while the latter in an imperative-style language.
Of course, this is just one feature and I haven’t even bothered discussing intersection types…