Union Types (PHP 8.0)
debt(d5/e1/b3/t3)
Closest to 'specialist tool catches it' (d5). The detection_hints list phpstan, psalm, and rector — all specialist static analysis tools. The common mistake of using mixed instead of a precise union type, or relying on docblocks only, is caught by these tools rather than by compiler or default linter. No default linter catches this automatically.
Closest to 'one-line patch or single-call swap' (e1). The quick_fix explicitly states: use int|string instead of mixed — a single-declaration change at the function signature level. Swapping mixed or a docblock annotation for a native union type is a one-line replacement per site.
Closest to 'localised tax' (b3). Union types apply across web, cli, and queue-worker contexts but the choice is localised to individual function/method signatures. Misuse (e.g. using mixed instead of a union) imposes a tax only on the components where those signatures live; the rest of the codebase is largely unaffected. Not a cross-cutting architectural burden.
Closest to 'minor surprise (one edge case)' (t3). The misconception field identifies one notable edge case: developers assume ?string and union types are equivalent, not realising ?string is strictly shorthand for string|null only and union types are a strict superset. Also, int|null vs ?int is a style confusion. These are minor surprises rather than catastrophic misunderstandings — a competent developer learns them quickly.
Also Known As
TL;DR
Explanation
Union types (PHP 8.0) enable expressing that a value can be one of several types without resorting to mixed. For example, function getUser(int|string $id) accepts either an integer or string ID. The special null type can be included (int|null or the shorthand ?int for single types). PHP 8.0 also introduced the mixed pseudo-type as an explicit any. Union types interact with strict_types and are checked at runtime, throwing TypeError on mismatch.
Common Misconception
Why It Matters
Common Mistakes
- Using mixed when a specific union like int|string|null is known — mixed disables type checking entirely.
- Not handling all cases in the receiving code — a union type without exhaustive handling defeats the purpose.
- Using union types instead of a proper abstraction — if a function accepts User|Admin, consider a common interface.
- Forgetting that int|null is equivalent to ?int — both are valid but ?Type is more concise for nullable.
Avoid When
- Do not use union types as a workaround for poor design — a parameter that accepts 5 different types usually needs refactoring.
- Avoid mixed as a union type shortcut — it opts out of type checking entirely.
When To Use
- Use union types to accurately express a parameter or return that can legitimately be one of several types.
- Use int|string for IDs that may come from URLs (string) or database rows (int).
Code Examples
// mixed type — no type checking:
function process(mixed $input): mixed { /* anything goes */ }
// Union type — explicit and checkable:
function process(int|string $id): User|null {
if (is_int($id)) return User::findById($id);
return User::findBySlug($id);
}
// PHP 8.0 union types — accept multiple types
function formatId(int|string $id): string {
return (string) $id;
}
// With null shorthand (PHP 7.1+)
function find(int $id): ?User { return null; } // = User|null
// PHP 8.0 — null in union
function process(int|null $value): void {}
// PHP 8.1 intersection types — value must satisfy ALL types
function handle(Countable&Iterator $collection): void {}
// PHP 8.2 DNF types — combination of union and intersection
function accept((Countable&Iterator)|array $items): void {}