The key insight is that there are two ways of modeling domain (broadly speaking).
Let's take classes and interfaces. Say you declare IShape and make Circle, Rectangle, and all others implement it. Great. What methods are you going to have on IShape? Let's say Draw. So far so good.
Now imagine you implement yourself a dozen shapes. And then, a few months later, you find yourself in need of another operation. Let's call it IsEmpty. What do you do? You add IsEmpty to IShape, and then you go into each of the dozen classes and add IsEmpty to them. A bit of a hassle, but ok, you can do it.
A few months down the road you want to add another operation. Then another. And another. You get tired of this pretty quickly, but that's still fine, you grit your teeth, but you do it.
But then there's the next problem: somebody else who is using your library wants to add an operation of their own. What do they do? They can't modify the IShape interface, it's in your library. They can ask you to do it and republish the library (not very efficient, is it?). Or they can implement the operation in terms of if + is - i.e. if (shape is Circle) { ... } else if (shape is Rectangle) { ... }, and so on. But then they hit the very difficulty described in the article you linked - the compiler won't protect them from missing a shape!
On the other hand, take discriminated unions. You describe the union, and then add operations all you want, left and right. Every operation handles all cases within itself (and the compiler verifies that all cases are indeed handled), and you can even add new operations in other projects that reference your library without modifying the original code. Total nirvana!
But then, a few months down the road, you find out that you need another case - say Triangle. You can, of course, add this case to the type, but then you'd have to go and add handling for it in every single operation. Even worse: those people who are using your library - their code will break when they get the latest version, and they will have to modify their additional operations as well. Tedious!
So it seems that there are two different mutually exclusive ways:
- Either you make it easy to add new cases, but hard to add new operations (aka "open world" model).
- Or you make it easy to add new operations, but hard to add new cases (aka "closed world" model).
This is a well known problem in language design. So well known it has its own name - the "Expression Problem". There are actually languages that let you have your cake and eat it, too - Haskell has type classes, Clojure has protocols, Rust has traits, etc. Neither of the solutions that I've seen are elegant enough in practice, to the point of getting one to wonder whether solving the expression problem is even worth it.
F# does not solve this problem[1] - you can't have both ways at the same time. However, F# at least supports both ways separately: classes+interfaces for "open world", discriminated unions for "closed world". C#, on the other hand, supports only "open world".
More importantly, it turns out that in real programs "closed world" modeling is far more useful than "open world". Programs modeled this way turn out to be far more understandable, less buggy, more concise. "Open world" models usually come in useful when you expect your program to be extended after it's written, by somebody you don't necessarily know - aka "plugins". This situation does happen, but not that often.
[1] if you don't count shenannigans with statically resolved type parameters, which don't reliably work in all circumstances anyway