Beyond 'mixed': Leveraging Union and Intersection Types in PHP 8.x
Stop relying on PHPDoc for complex types. Learn how to use PHP 8.x Union and Intersection types to enforce strict runtime contracts and reduce boilerplate code.
23 Mar 2026, 23:32 UTC

The Problem with Generic Type Hints
For years, PHP developers relied on mixed or PHPDoc comments (like @param string|int $id) to handle variables that could be multiple things. The problem is that PHPDoc is a suggestion, not a rule. If a developer ignores the comment and passes an array where a string or integer was expected, the error only surfaces deep inside the logic—often as a cryptic TypeError or a silent failure that corrupts data.
The takeaway is simple: PHP 8.0 and 8.1 introduced native Union and Intersection types to move these checks from documentation into the engine, allowing the runtime to kill invalid state before a single line of your method body executes.
Union Types: Handling Multiple Possibilities
Union types (PHP 8.0+) allow you to specify that a value can be one of several different types. This is particularly useful for legacy database IDs that might be integers or strings, or for methods that return a result object on success but a boolean false on failure.
Instead of writing a manual check like if (!is_string($val) && !is_int($val)), you define the type directly in the signature. This forces the caller to adhere to the contract, making the code self-documenting and safer.
Intersection Types: Enforcing Multiple Interfaces
While Union types are an "OR" relationship, Intersection types (PHP 8.1+) are an "AND" relationship. They are used exclusively with class types (usually interfaces). An intersection type requires that an object implements all listed interfaces.
This is a powerful tool for Dependency Injection. Instead of creating a massive "God Interface" that combines every possible method, you can keep your interfaces small and granular. You then tell your method exactly which combination of capabilities the passed object must possess.
Practical Implementation Example
Consider a system that processes a payment. The payment object must be both PaymentInterface (for processing) and LoggableInterface (for auditing). Here is how to implement this in PHP 8.1+.
<?php
interface PaymentInterface {
public function getAmount(): float;
}
interface LoggableInterface {
public function getLogMessage(): string;
}
class CreditCardPayment implements PaymentInterface, LoggableInterface {
public function getAmount(): float { return 99.99; }
public function getLogMessage(): string { return "Credit Card Transaction"; }
}
class Processor {
// Union Type: $id can be string OR int
// Intersection Type: $payment MUST be PaymentInterface AND LoggableInterface
public function process(string|int $id, PaymentInterface&LoggableInterface $payment): void {
echo "Processing ID: $id\n";
echo "Log: " . $payment->getLogMessage() . " for amount " . $payment->getAmount();
}
}
$processor = new Processor();
$payment = new CreditCardPayment();
// This works
$processor->process(12345, $payment);
$processor->process("TXN-789", $payment);
// This would throw a TypeError because the type is not an int or string
// $processor->process(["id" => 1], $payment);
?>Constraints and Trade-offs
These features are powerful, but they have strict limitations:
- Intersection limits: You cannot use intersection types with scalar types (like
string&int) because a value cannot be both a string and an integer simultaneously. - Complexity warnings: If you find yourself creating union types with four or five different classes (e.g.,
TypeA|TypeB|TypeC|TypeD), it is often a signal that your method is doing too much. This usually indicates a violation of the Single Responsibility Principle; consider introducing a common interface instead. - Compatibility: Introducing these types into a shared library will break backward compatibility for any project still running on PHP 7.x.
Verifying Your Implementation
To ensure these types are working as intended, you can perform three checks:
- Runtime Test: Pass an object that implements only one of the two required interfaces to an intersection-typed method. PHP should immediately throw a
TypeError. - Negative Union Test: Pass an array to a
string|intparameter. Verify the execution stops at the method call. - Static Analysis: Run a tool like PHPStan or Psalm. These tools read the native types and will flag potential type mismatches in your IDE before you even run the code.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.