Discriminated unions works best if there is no semantic overlap between the cases.
In your example, each case contains the same component with the same meaning, a string
indicating "the name of the animal". But that's a semantic overlap! The discriminating union will then force you to make distinctions you don't want to: You don't want to be forced to discriminate between the "name of a pig" and the "name of a cow"; you just want to think of "the name of an animal".
Let's make a type that fits better:
type Animal = Pig | Cow | Fish
type Pet = Animal * string
let animals = [(Pig, "Mike"); (Fish, "Eve"); (Pig, "Romeo")
With that type, filtering out non-Pig
s is a one-liner:
animals |> List.filter (fst >> (=) Pig)
If not every animal has a name, use an option type:
type Pet = Animal * string option
You would use a discriminated union for your animals if you knew that, say, every Pig
has a name, but no Fish
does: those cases have no overlap.