0

PHP 8.1 — Intersection types

Advanced5 min read·php-09-013

Concept

Intersection types (PHP 8.1) allow a parameter or return type to require that the value implements ALL specified interfaces simultaneously. The syntax is TypeA&TypeB. The value must satisfy every type in the intersection.

Difference from union types: Union types (A|B) mean "either A or B." Intersection types (A&B) mean "both A and B." A value satisfying an intersection type is more specific than one satisfying either constituent alone.

Use case — multiple interface requirements: When a function genuinely needs an object to be both Countable AND Iterable, Countable&Iterable expresses this precisely. Without intersection types, you'd need to create a new interface extending both, or use mixed and add a PHPDoc comment.

DNF types (PHP 8.2): Disjunctive Normal Form types combine unions and intersections: (Countable&Traversable)|null. An intersection must be wrapped in parentheses inside a union.

Limitations:

  • Cannot contain a class in the intersection (only interfaces). User&Loggable is invalid because User is a class.
  • Cannot combine with null directly (A&B|null requires DNF: (A&B)|null).
  • Cannot use scalar types (int&string makes no sense — a value can't be both).

Code Example

php
<?php
declare(strict_types=1);

interface Countable2
{
    public function count(): int;
}

interface Iterable2
{
    public function toArray(): array;
}

interface Stringable2
{
    public function __toString(): string;
}

// Intersection type — must implement BOTH interfaces
function processCollection(Countable2&Iterable2 $collection): void
{
    echo "Count: " . $collection->count() . "\n";
    foreach ($collection->toArray() as $item) {
        echo "  - $item\n";
    }
}

class StringCollection implements Countable2, Iterable2
{
    private array $items;
    public function __construct(string ...$items) { $this->items = $items; }
    public function count(): int { return count($this->items); }
    public function toArray(): array { return $this->items; }
}

$coll = new StringCollection('PHP', 'Laravel', 'MySQL');
processCollection($coll); // works — StringCollection implements both

// Only Countable2 — missing Iterable2
class CountOnlyCollection implements Countable2
{
    public function count(): int { return 0; }
}
// processCollection(new CountOnlyCollection()); // TypeError

// Triple intersection
function displayItem(Countable2&Iterable2&Stringable2 $item): void
{
    echo (string) $item . "\n";
    echo "Has " . $item->count() . " sub-items\n";
}

// DNF types (PHP 8.2) — intersection within a union
function handleMaybeCollection((Countable2&Iterable2)|null $collection): int
{
    return $collection?->count() ?? 0;
}

// Real-world: repository + cache combined
interface Repository { public function find(int $id): ?object; }
interface Cacheable  { public function getCacheKey(): string; }

function findWithCache(Repository&Cacheable $source, int $id): ?object
{
    $key = $source->getCacheKey() . ':' . $id;
    // Try cache first, then source
    return cache()->remember($key, 3600, fn() => $source->find($id));
}