PHPUnion & Intersection Types

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
<?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
<?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
<?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
<?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
<?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.

mixed is not the same as omitting a type hint
A parameter with no type hint at all still behaves like `mixed` at runtime, but tooling treats the two differently: static analyzers flag an untyped parameter as a potential oversight, while an explicit `mixed` is treated as intentional and produces no such warning. If a function genuinely accepts any type, prefer writing `mixed` explicitly rather than leaving the hint off.
  • int|string (union) means "any one of these types"; Countable&Iterator (intersection) means "all of these types at once."

  • ?Type is exactly equivalent to Type|null — pure shorthand, not a separate feature.

  • mixed is 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.

  • never marks a function that always throws, exits, or otherwise never returns control to its caller — useful for static analysis of unreachable code.

Tip
When a union type grows past two or three members, it's often a sign the function is doing too many unrelated things depending on the input's shape. Consider whether separate, more specific functions — or a shared interface the callers already implement — would communicate the intent more clearly than a long union.