Examples. The first few are in C, the last couple in Haskell because they need it.
At the most simple, consider a function that I'm calling like `compute_thing(bid, ask)`. Maybe that's in error, and it actually expects `(ask, bid)`. If it was defined to take primitives (`int` or whatnot) then that's a logical error the types don't catch. But if I wrap those primitives in a struct to get nominal typing (`typedef struct ask { int v; } ask_t`), it's caught.
This is only a small improvement. In code that's well exercised, this will probably be found by tests, and certainly you should make sure you have coverage. But an obscure corner case may only be run in tests where bid == ask, and then the values won't tell you anything's wrong. A bad value also doesn't tell you where the error is, whereas this use of nominal typing points you at the exact source location. (Named parameters would also catch this particular issue, where supported and used).
Slightly more complicated, give the same treatment to indexes. `foos[idx]` versus `lookup_foo(idx)` with a nominally-typed argument. In the former case, there is room for me to have confused which index was talking about which array. For tests, this is possibly worse than the above case, because it's quite common for small tests to involve few elements, which means indexes are more likely to spuriously agree.
Now let's get tricky. I had a few threads (statically assigned to various roles, not a pool) managing resources they own. Latency on the order of microseconds was vitally important, so couldn't have threads waiting around on locks. I had to be careful that a function called on one thread didn't touch a resource owned by another thread.
If my access functions demanded a nominally typed token I passed between functions, I could detect exactly where I called something from an unintended thread. If this token took the form of an empty struct, all this checking happened at compile time with no runtime overhead at all (the exact same bytecode was emitted if I manually stripped out the tokens), and completely eliminated one kind of concurrency bug (which are often very difficult to detect and localize with tests). This made it very easy to move functionality between threads - I could make a small change and my compiler would point out specific locations incompatible with that change.
Moving to Haskell, I had a Markov text generator, where the models were represented as a map from lists of strings to maps from strings to counts (`Map [String] (Map String Int)`). Merging two models was easy (`M.unionWith (M.unionWith (+))`) but if the two models looked back a different number of tokens then the result would be broken. I could hang that value on them as data, and check it manually... or I could carry it in the type and let the compiler check it for me.
In a more serious context, I was evaluating SQL I had parsed. I would produce "I have evaluated this bit of it" results that I could thread together. Adding a phantom parameter to their type, I could distinguish between "this is something that operates at the table level" and "this is something that operates at the row level" and I could not merge them without them being of the same type.
I then wrote functions to convert between the two. The one to take a "table" value to a "row" value (as one might when evaluating a sub-select in an expression) was a no-op. The one to go other way (evaluating an expression in a selection list or WHERE or ORDER BY or ...) I wrote to demand that I introduce the values present in the current row.
So consider: I have lists and sometimes I forget to add them to my dictionary in the right places - a logical error if ever there was one. Now my type checker will catch it.