Union & Intersection Types
Type declarations in PHP used to force an all-or-nothing choice: either a parameter accepted exactly one type, or you dropped the type hint entirely and accepted anything. That gap forced a lot of real-world signatures — a function that legitimately accepts either an int or a string, say — to give up on typing altogether. PHP 8.0's union types and PHP 8.1's intersection types closed that gap, letting a single declaration describe "one of several types" or "all of several interfaces at once" precisely.
Union types: one of several types
A union type is written as two or more types joined with |, and it means the value must be an instance of at least one of them. Union types can appear on parameters, return types, and typed properties.
A function that accepts int or string
<?php
function formatId(int|string $id): string
{
return 'ID-' . $id;
}
echo formatId(42), "\n";
echo formatId('legacy-042'), "\n";ID-42 ID-legacy-042
Before union types existed, this signature had two unappealing options: drop the type hint and accept mixed implicitly (losing all safety), or pick just one of int/string and force callers to cast before calling. The union type lets the signature say exactly what it means — no more, no less.
The nullable shorthand ?Type
A very common union is "this type, or null." PHP provides a dedicated shorthand for exactly that case: prefixing a type with ?. ?Type is pure syntax sugar for Type|null — the two are completely interchangeable, and PHP treats them identically internally.
?Type and Type|null are the same thing
<?php
function findUser(int $id): ?User
{
return $id === 1 ? new User('Amara') : null;
}
// Exactly equivalent to the line above:
function findUserAlt(int $id): User|null
{
return $id === 1 ? new User('Amara') : null;
}Most PHP style guides prefer ?Type when the union is exactly two members and one of them is null, reserving the explicit Type|null spelling for unions that already have other members, such as int|string|null, where there's no shorthand available.
mixed: an explicit "accepts anything"
PHP 8.0 also introduced mixed as a real type, representing the union of every possible type. It looks similar to omitting a type hint altogether, but there's a meaningful difference: mixed is a deliberate, visible statement that a parameter genuinely accepts anything, whereas an untyped parameter usually just means nobody got around to typing it. mixed also can't be narrowed — a subclass can't override a mixed parameter with a more specific type, since mixed already permits everything.
mixed as an intentional declaration
<?php
function debugDump(mixed $value): void
{
var_dump($value);
}
debugDump(42);
debugDump('hello');
debugDump([1, 2, 3]);Intersection types: all of several interfaces
PHP 8.1 introduced the inverse idea: instead of "one of these types," an intersection type written as TypeA&TypeB requires a value to satisfy every listed type simultaneously. Because a class can implement multiple interfaces but PHP only supports single inheritance for classes, intersection types are restricted to interfaces — you can't intersect two concrete classes.
Requiring both Countable and Iterator
<?php
function summarize(Countable&Iterator $collection): void
{
echo 'Items: ' . count($collection) . "\n";
foreach ($collection as $item) {
echo '- ' . $item . "\n";
}
}Without an intersection type, expressing "must be both Countable and Iterator" required either two separate parameters, a runtime instanceof check inside the function body, or a new interface created purely to combine the two — extra ceremony for something the type system can now say directly.
never: a return type for functions that never return
PHP 8.1 added the never return type for functions that are guaranteed to never complete normally — every code path either throws an exception, calls exit()/die(), or otherwise never reaches a return statement. This is different from a void function, which does return (just without a value); a never function promises it won't return control to its caller at all.
never for a function that always throws
<?php
function assertPositive(int $value): void
{
if ($value <= 0) {
throwError('Value must be positive');
}
echo "Value {$value} accepted\n";
}
function throwError(string $message): never
{
throw new InvalidArgumentException($message);
}Static analysis tools use never to understand control flow more precisely — if throwError() is declared never, a tool like PHPStan knows that any code after a call to it is unreachable, and that assertPositive()'s echo line only runs when $value is actually positive, without needing to trace the exception logic itself.
int|string(union) means "any one of these types";Countable&Iterator(intersection) means "all of these types at once."?Typeis exactly equivalent toType|null— pure shorthand, not a separate feature.mixedis a real, explicit type meaning "accepts anything," distinct from simply skipping a type hint.Intersection types can only combine interfaces, not concrete classes, because PHP classes support only single inheritance.
nevermarks a function that always throws, exits, or otherwise never returns control to its caller — useful for static analysis of unreachable code.