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.