I often find them handy for locking down admissible states at compile time. Maybe ~10 years ago in a processor design class, I wrote some CPUs in Haskell/Clash for FPGA usage. A nice thing I could do was write a single top-level instruction set, but then lock down the instructions based on what stages of the processor they could exist at.
For example, something like (not an actual example from my code, just conceptually - may be misremembering details):
data Instruction stages where
MovLit :: Word64 -> Register -> Instruction '[Fetch, Decode, Execute, Writeback]
-- MovReg instruction gets rewritten to MovLit in Execute stage
MovReg :: Register -> Register -> Instruction '[Fetch, Decode, Execute]
...
And then my CPU's writeback handler block could be something like:
writeback :: (Writeback `member` stages) => Instruction stages -> WritebackState -> WritebackState
writeback (MovLit v reg) = ...
-- Compiler knows (MovReg _ _) is not required here
So you can use the type parameters to impose constraints on the allowed values, and the compiler is smart enough to use this data during exhaustiveness checks (cf "GADTs Meet Their Match")