Why Is instanceof *Type Wrong and Getting Deprecated?
February 8, 2023 · 8 min read
This one is for developers of custom rules and other extension types. PHPStan can be extended by writing your own code. Many users take advantage of that and write custom rules for their proprietary code. Some of the most notable public extensions are phpstan-doctrine and Larastan. PHPStan also serves as the foundation for tools like Rector. The points and changes described below are important for developers of all those packages.
The upcoming release of PHPStan 1.10 is going to deprecate using instanceof *Type
for many Type interface implementations. We have many good reasons to do so. If you rewrite your code based on the guidelines below, you’re going to have edge cases solved for free, and therefore have less bugs. By doing the same thing in PHPStan itself we’ve also fixed many bug reports.
StringType #
Let’s say you’re writing a custom rule and you want to check something about strings. If you follow dump-driven development [1], you would write a piece of code to test your rule against, and in that rule you would var_dump()
the type representing an expression to figure out how to implement the rule:
public function doFoo(string $input): void {
// yeah, the type of expr in PhpParser\Node\Stmt\Echo_ is PHPStan\Type\StringType
echo $input;
}
In the rule we will not continue unless the type is StringType:
if (!$type instanceof StringType) {
return [];
}
// ...
Looks reasonable, right? But PHPStan 1.10 with bleeding edge enabled or with phpstan-deprecation-rules installed will report the following error thanks to a new rule:
Doing instanceof PHPStan\Type\StringType is error-prone and deprecated. Use Type::isString() instead.
Why is it error-prone? Because not all strings are represented with just a StringType instance. Many strings, like non-empty-string
, are actually an IntersectionType of StringType and some other accessory type. So in case of some strings, your straightforward check with instanceof StringType
wouldn’t work as expected.
For a reliable check there’s a new method PHPStan\Type\Type::isString(): TrinaryLogic
. Why does it return TrinaryLogic? Because it’s useful to know that:
- This can’t be a string (e.g. an integer, an object)
- This might be a string (e.g.
mixed
,int|string
) - This is a string (e.g.
string
,non-empty-string
,'a'
)
In most situations you would be interested in the last case with $type->isString()->yes()
but it’s useful to be aware of all possibilities.
ConstantStringType #
Another example of a deprecated check is instanceof ConstantStringType
which represents a literal string like 'lorem ipsum'
. The common pattern for working with it is:
if (!$type instanceof ConstantStringType) {
return [];
}
// do something with $type->getValue() (which is 'lorem ipsum')
PHPStan 1.10 wants you to use Type::getConstantStrings(): list<ConstantStringType>
. It’s plural because it wants you to handle unions of different strings. So after the refactoring your code is going to be able to handle 'lorem ipsum'|'dolor sit amet'
as well:
if (count($type->getConstantStrings()) === 0) {
return []; // no constant strings
}
foreach ($type->getConstantStrings() as $constantString) {
// do something with each value
}
ArrayType #
Doing instanceof ArrayType
comes with the same gotchas as StringType
described above. When you have a non-empty-array
type or a list
, they’re actually an IntersectionType
.
You can replace instanceof ArrayType
with Type::isArray(): TrinaryLogic
.
ObjectType #
Doing instanceof ObjectType
breaks in many situations. Objects can be part of intersection types, objects usually come in unions, object can be represented by a completely different class when it’s a static
type or $this
.
Use Type::isObject(): TrinaryLogic
to find out if the type is an object, or Type::getObjectClassNames(): list<string>
to get a list of class names for a given object type.
You can also use Type::isSuperTypeOf(): TrinaryLogic
to find out whether an object is of a specific class or its subclass:
// is YES for DateTimeInterface, DateTime, DateTimeImmutable
$isDateTime = (new ObjectType(DateTimeInterface::class))->isSuperTypeOf($type);
Learn more about isSuperTypeOf()
in the documentation »
CallableType #
Many things in PHP can be a callable, not just the callable
typehint. Doing instanceof CallableType
will miss out on many of these types:
- A string with a function name like
'date'
is a callable - An array with an object and a method like
[$this, 'method']
is a callable - An array with a class name and a static method like
[Foo::class, 'method']
is a callable - A
Closure
is a callable - An object of a class with an
__invoke()
method is a callable
If you ask Type::isCallable(): TrinaryLogic
it will account for all of these types. And you can get the actual callable signatures with Type::getCallableParametersAcceptors(): ParametersAcceptor[]
as well.
ConstantBooleanType #
Surely true
or false
doesn’t get any more complicated and asking instanceof ConstantBooleanType
always works, right!?
Let me break that for you:
/**
* @template T
* @param T $b
* @return T
*/
public function sayHello($b)
{
if ($b === true) {
// T&true = IntersectionType
\PHPStan\dumpType($b);
}
}
It’s an IntersectionType
because we need to carry that $b
is still the template type T
while at the same time we narrowed it down to true
.
This is why you should use Type::isTrue(): TrinaryLogic
and Type::isFalse(): TrinaryLogic
.
This isn’t an exhaustive list of all types where doing instanceof *Type
is deprecated in PHPStan 1.10, just the most notable examples. For a complete list please refer to the actual rule source code. This list is going to grow bigger in future PHPStan releases.
Don’t worry, this is currently just being deprecated. If you have instanceof *Type
in your code, it’s still going to work for the scenarios where it has already worked. But as you can see, switching to the new methods can be very beneficial.
We’re deprecating it now so that we have free hands to do more changes in future major PHPStan releases. Not relying on instanceof
to work will allow us to stop using inheritance in the typesystem. So for example ConstantStringType
might no longer extend StringType
in PHPStan 2.0.
Once we deprecate and eradicate $type instanceof *Type
from PHPStan itself and all 3rd party PHPStan extensions as well, we’ll finally be able to decouple types from each other, which will allow us to do cool stuff, e.g. support all template type bounds automatically [2], or make all types subtractable automatically [3] as well.
If you’ve just started working with the AST and with PHPStan’s typesystem, you don’t have much choice than to start orientating yourself by
var_dump
-ing what’s going on. ↩︎Right now each bound (like
@template T of int
) requires to have a custom class likeTemplateIntegerType
, so that both$type instanceof IntegerType
and$type instanceof TemplateType
pass. ↩︎Only few selected types are currently subtractable, like
mixed
orobject
. Playground example ↩︎