Union Types
What Are Union Types?
In simple terms: Sometimes a value can legitimately be one of several types. A product lookup might accept either a numeric ID (7) or a SKU string ('PEN-01'); a function like strpos() returns an integer position or false. A union type says exactly that: "this value is an int or a string".
Technically: Union types, introduced in PHP 8.0, are written as types separated by a vertical bar: int|string. They can be used for parameters, return values, and properties. A value is accepted if it matches any of the listed types. PHP 8.1 added intersection types (A&B — must match all listed types), and PHP 8.2 added DNF types, which combine both ((A&B)|null).
Why Union Types Were Needed
Before PHP 8.0, a parameter that accepted more than one type could not be typed at all. The intended types existed only in docblocks (@param int|string $id), which PHP did not enforce. Union types turn that documentation into checked code.
Syntax
function find(int|string $idOrSku): Product|false { ... }
class Invoice
{
private int|float $amount;
}
A First Example
<?php
function describe(int|float|string|bool|null $value): string
{
return get_debug_type($value) . ': ' . var_export($value, true);
}
foreach ([42, 3.14, 'PHP', true, null] as $value) {
echo describe($value) . "\n";
}
try {
describe([1, 2]);
} catch (TypeError $e) {
echo strstr($e->getMessage(), ', called', true) . "\n";
}
Output
int: 42
float: 3.14
string: 'PHP'
bool: true
null: NULL
describe(): Argument #1 ($value) must be of type string|int|float|bool|null, array given
- Each value matches one member of the union, so all five calls succeed.
- An array matches none of them, so PHP throws a
TypeError. The message lists the accepted types in PHP's internal order, which may differ from the order you wrote. get_debug_type()returns a readable type name, which is handy when working with unions.
Handling Each Type Inside the Function
A union-typed parameter usually needs different handling per type. Use type checks to branch:
<?php
declare(strict_types=1);
function findProduct(int|string $idOrSku): string
{
return is_int($idOrSku)
? "Looking up product by ID $idOrSku"
: 'Looking up product by SKU ' . strtoupper($idOrSku);
}
echo findProduct(7) . "\n";
echo findProduct('pen-01') . "\n";
Output
Looking up product by ID 7
Looking up product by SKU PEN-01
After is_int(), both PHP and static analysis tools know which type is in use in each branch. match (true) with is_*() checks or instanceof is a common pattern for unions with more members.
Coercion With Union Types
In strict mode, a value must match one of the types exactly (with the usual int-to-float exception). In coercive mode, if no type matches exactly, PHP tries conversions in a fixed order of preference: int, float, string, bool.
<?php
function toNumber(int|float $value): int|float
{
return $value;
}
var_dump(toNumber('42'));
var_dump(toNumber('4.2'));
var_dump(toNumber('1e3'));
var_dump(toNumber(true));
Output
int(42)
float(4.2)
float(1000)
int(1)
'42'is an integer string, so it becomes anint.'4.2'and'1e3'are float strings, so they becomefloats.truebecomes1, becauseintis preferred.
Because these rules are subtle, unions are clearest in strict mode, where no conversions happen.
The false and true Types
false can be used in unions to describe the classic PHP pattern "a result, or false on failure":
<?php
function findPosition(string $haystack, string $needle): int|false
{
return strpos($haystack, $needle);
}
var_dump(findPosition('GoCourse', 'Course'));
var_dump(findPosition('GoCourse', 'PHP'));
Output
int(2)
bool(false)
Since PHP 8.2, false, true, and null can also be used as standalone types (for example, a method that always returns true). For new code, returning null (with a nullable type) or throwing an exception is usually clearer than returning false.
Intersection Types (PHP 8.1)
An intersection type requires a value to satisfy all of the listed class or interface types:
<?php
function summarize(Countable&Traversable $items): string
{
$parts = [];
foreach ($items as $item) {
$parts[] = $item;
}
return count($items) . ' items: ' . implode(', ', $parts);
}
echo summarize(new ArrayIterator(['Pen', 'Notebook', 'Lamp'])) . "\n";
Output
3 items: Pen, Notebook, Lamp
The function needs to count and loop, so it requires an object that is both Countable and Traversable. ArrayIterator implements both. An array would be rejected, because arrays are not objects implementing these interfaces. Intersection types can only contain class and interface types.
DNF Types (PHP 8.2)
Disjunctive Normal Form types combine intersections and unions — "this intersection or that type":
function summarize((Countable&Traversable)|array $items): string { ... }
Now the function accepts arrays as well as countable, traversable objects. Intersections must be written in parentheses.
Rules and Restrictions
| Rule | Invalid Example | Valid Alternative |
|---|---|---|
| No duplicate or redundant types | int|int | int |
? cannot be combined with a union | ?int|string | int|string|null |
void cannot be part of a union | void|int | ?int |
mixed cannot be part of a union | mixed|null | mixed (it already includes null) |
never can only be used alone | never|int | never or int |
These errors are reported at compile time, as soon as the file is loaded.
When to Use Union Types
| Good Use | Consider an Alternative |
|---|---|
Values that are genuinely one of a few types (int|string IDs) | Large unions such as int|float|string|bool|array usually signal a design problem |
Describing existing APIs (int|false) | For new APIs, prefer ?int or exceptions over false |
Accepting string|Stringable | For several object types with shared behavior, define an interface |
Number types (int|float) | A value object (Money) when the value has rules |
Common Mistakes
Using Unions Instead of Interfaces
Effect: A parameter typed EmailNotifier|SmsNotifier|WhatsAppNotifier must be changed for every new notifier.
Fix: Define a Notifier interface.
Forgetting to Handle Every Type
Effect: Code assumes one member of the union and fails for others.
Fix: Branch explicitly with is_*() checks, instanceof, or match (true).
Relying on Coercion Order
Effect: Surprising conversions in coercive mode.
Fix: Use strict types.
Writing ?A|B
Effect: Compile error.
Fix: Write A|B|null.
Best Practices
- Use union types to describe values that are legitimately of several types.
- Keep unions small; prefer interfaces for families of classes.
- Handle each possible type explicitly inside the function.
- Use intersection types when an object must fulfill several interfaces.
- Prefer
nullor exceptions overfalsefor failures in new APIs.
Official reference: https://www.php.net/manual/en/language.types.type-system.php