The engine rejects a union containing the same type twice, and the check happens at compile time without loading any classes, so two aliases for one class pass.
use VendorMoney as VendorMoney;
use AppMoney; // suppose these resolve the same
function f(Money|VendorMoney $m) {} // accepted
function g(int|int $x) {} // Fatal: duplicate type
function h(int|INT $x) {} // Fatal — case-insensitive
// and the redundancy check is structural, not semantic:
function i(iterable|array $x) {} // Fatal, array ⊂ iterable
function j(Countable|Traversable $x) {} // fine
The deduplication rules read as inconsistent until you know they are all resolved without autoloading: scalar types and the built-in aliases are known to the compiler, and class names are just strings until something instantiates them. The iterable|array rejection catches a genuine mistake and the aliased-class case cannot be caught at all, so an analyser is the only thing that will tell you.