If you've ever written @param int|string $id in PHPDoc, you've already met union types. But PHP 8 makes them native, removing all ambiguity. And with PHP 8.1 comes the opposite: intersection types. We at Meteora Web have worked on PHP codebases for over 8 years — we've seen entire projects built on docblocks and manual checks that could have been solved with two well-placed characters. This guide takes you into the heart of PHP 8's type system, no fluff, with code you can copy today.
What Are Union Types and Why Use Them?
A union type declares that a value can be one of several types. In PHP 7.4 you could already use ?int for int|null, but for other combinations you had to rely on comments. With PHP 8.0 you write directly:
public function find(int|string $id): ?User { ... }
The advantage is immediate: the engine verifies the type at runtime and a modern IDE (PhpStorm, VS Code with intelephense) knows exactly what to expect. No more vague mixed or cascading checks. If your function accepts a numeric ID or a textual slug, union types are the right path.
Sponsored Protocol
Return types with unions
function fetchCustomer(int|string $identifier): Customer|false { ... }
Here false is a valid scalar type (typical pattern of built-in functions). In a real project, we prefer returning null instead of false — but if you work with legacy code, Customer|false is already a step up from mixed.
How to Use Intersection Types in PHP 8.1?
Intersection types (introduced in PHP 8.1) represent the exact opposite: a value must satisfy all types simultaneously. They are written with & and work only with object types (classes, interfaces, traits).
interface Renderable {
public function render(): string;
}
interface Loggable {
public function log(string $message): void;
}
function process(Renderable&Loggable $entity): void {
echo $entity->render();
$entity->log('processed');
}
The function process only accepts objects that implement both interfaces. If you pass an object implementing only Renderable, PHP throws a TypeError at runtime. Intersection types are perfect for combining contracts without multiple inheritance — much cleaner than abstract classes full of instanceof.
Sponsored Protocol
What Are the Differences Between Union and Intersection Types?
The difference is logical:
- Union (|): “A or B” – at least one type must be satisfied.
- Intersection (&): “A and B” – all types must be satisfied simultaneously.
Unions apply to any type (scalars, objects, null). Intersections work only on object types because a value cannot be simultaneously an integer and a string. Moreover, intersecting two classes that do not share a hierarchy is logically impossible — PHP signals it at compile time.
How to Handle null with Union Types?
PHP 8 made ?Type an alias of Type|null. You can write either:
public function getTitle(): ?string { ... }
// Equivalent:
public function getTitle(): string|null { ... }
We recommend using ?Type when it's a single union with null, and Type1|Type2|null when the combination is more complex. Readability matters.
Sponsored Protocol
Practical Examples of Union and Intersection Types in a Real Project
Case 1: Generic Repository with Multiple Identifiers
interface EntityRepository
{
public function find(int|string $id): ?Entity;
public function findByCriteria(string $field, int|string|float $value): array;
}
In one of our client platforms (Laravel/Livewire) we handle users with numeric IDs or UUIDs. One interface covers both cases without duplicating methods.
Case 2: Validator with Interface Intersection
interface Validatable {
public function validate(): bool;
}
interface Persistable {
public function save(): void;
}
function saveIfValid(Validatable&Persistable $entity): void {
if ($entity->validate()) {
$entity->save();
}
}
The method saveIfValid only accepts entities that can be validated and saved. If tomorrow an object implements only Validatable, the compiler blocks it immediately, not at runtime after hours of testing.
Sponsored Protocol
Case 3: API Response with Error
function apiResponse(): array{status: string, data?: array|string}
This isn't a true union type on a property, but in an associative array you can use types similarly. For real safety, better create a typed DTO.
Common Mistakes to Avoid with Compound Types in PHP 8
- Intersection with a scalar type:
string&intmakes no sense — PHP throws a parse error at compile time. Remember: only object types. - Redundant unions:
int|string|intis valid but useless. PHP doesn't duplicate types, but clean up your code. - Forgetting explicit
null: If a function can returnnull, always declare it.?stringorstring|null, never juststringif there's a chance. - Confusing
|with&: Reading quickly, one slash can be misleading. We always put spaces between types:int | string(PHP accepts spaces).
What to Do Next
- Review your function signatures that use
mixedor@paramin docblocks. Convert them to union types where the possible cases are known (max 2-3 types). - Create a marker interface if you need intersection types: e.g.,
ExportableandPrintable, then useExportable&Printablewhere both are required. - Update your phpstan/psalm to the highest level: native types reduce false positives from static analyzers and make code self-documenting.
- Read the official PHP documentation for details on covariance/contravariance rules: PHP Type Declarations.
- If you use Laravel, take advantage of unions in custom form requests and repositories. We've written a more extensive guide in our PHP 8 Advanced Pillar — it also covers performance and async.
Types are not bureaucracy. They are contracts that the compiler enforces for free. Starting today with union and intersection types saves you hours of debugging tomorrow.