Intersection types

An annotation of the form Type1&Type2&Type3 is an Intersection Type. Any value must satisfy Type1, Type2 and Type3 simultaneously. Type1, Type2 and Type3 are all atomic types.

For example, after this statement in a PHPUnit test:

<?php

$hare = $this->createMock(Hare::class);

$hare will be an instance of a class that extends Hare, and implements \PHPUnit\Framework\MockObject\MockObject. So $hare is typed as Hare&\PHPUnit\Framework\MockObject\MockObject. You can use this syntax whenever a value is required to implement multiple interfaces.

Another use case is being able to merge object-like arrays:

/**
 * @psalm-type A=array{a: int}
 * @psalm-type B=array{b: int}
 *
 * @param A $a
 * @param B $b
 *
 * @return A&B
 */
function foo($a, $b) {
    return $a + $b;
}

The returned type will contain the properties of both A and B. In other words, it will be {a: int, b: int}.

Intersections of objects are only valid for lists of only object types and lists of only object-like arrays.

Scalar types can be intersected too: lowercase-string&non-empty-string is a non-empty-lowercase-string, lowercase-string&non-falsy-string a non-falsy lowercase string, numeric-string&non-falsy-string a numeric string other than '0', lowercase-string&numeric-string a numeric string such as '1e3' but not '1E3', int<0, max>&int<min, 10> an int<0, 10>, int&int-mask<1, 2> is 0|1|2|3, array-key&string is string and string&callable is a callable-string. Types referring to class constants, such as value-of<Foo::BAR>&int or (Foo::A|Foo::B)&int<2, 5>, are intersected once the constants are known. An intersection Psalm has no exact type for (e.g. literal-string&non-falsy-string) is reported as an invalid docblock, and so is one no value can have (e.g. int<0, 5>&int<6, 10>).

A conditional type can be intersected with other types: (T is int ? A : B)&C is (T is int ? A&C : B&C), a branch no value can have becoming never.

You can also intersect a class with an object shape to describe properties the class doesn't declare. This helps with classes that read properties through the magic __get method, where Psalm can't know which properties exist:

<?php

class Row
{
    /** @param array<string, mixed> $columns */
    public function __construct(private array $columns) {}

    public function __get(string $name): mixed
    {
        return $this->columns[$name] ?? null;
    }
}

/** @param Row&object{email: string} $user */
function sendWelcomeEmail(Row $user): void
{
    mail($user->email, 'Welcome!', 'Thanks for signing up.');
}

With a plain Row, $user->email would be mixed. With Row&object{email: string} it is a string.

Psalm takes a property from the shape only when the class doesn't declare it. The shape takes priority over a property that comes from a @mixin. A final class can only have undeclared properties through __get, so for a final class without __get (its own or inherited) Psalm still reports UndefinedPropertyFetch.