#1888·pest

`expect()->toBeInstanceOf()` / `not->toBeNull()` / `toBeString()` etc. don't narrow types like PHPUnit's assert methods`

Author: sebj54Created Aug 25, 2026Updated Sep 2, 2026

This issue has been made with Claude's help, but it's a real human asking

Context

PHPUnit's assertInstanceOf(), assertNotNull(), assertIsString()/Int/Float/Array/Numeric carry @phpstan-assert annotations directly in phpunit/phpunit's Assert class (e.g. @phpstan-assert null $actual on assertNotNull). PHPStan's core reads these natively and narrows the variable's type in the Scope for every subsequent line.

The Pest equivalents (expect($x)->toBeInstanceOf(Y::class), expect($x)->not->toBeNull(), expect($x)->toBeString(), …) don't do this. I checked pest-plugin-phpstan's source and found RedundantExpectationRule / ImpossibleExpectationRule (backed by MatcherAssertionRegistry), which read the scope's already-known type to flag a redundant/impossible expectation — but no class in the package implements PHPStan's TypeSpecifyingExtension, so nothing ever writes a narrowed type back into scope.

Impact

In a large test suite (Laravel/Larastan, level 9), converting $this->assertInstanceOf(...) / $this->assertNotNull(...) to their Pest expect() equivalents breaks static analysis on every line afterward that reads the now-unnarrowed value (Cannot access property $x on Y|null, ... on mixed, etc.). We ended up having to keep ~130 PHPUnit-style assertions specifically for this reason, everywhere the asserted value is read again.

Question

Is this a deliberate scope limitation, or something that could be implemented via a TypeSpecifyingExtension for the matchers in MatcherAssertionRegistry::METHOD_ASSERTIONS (toBeString, toBeInt, toBeFloat, toBeArray, toBeInstanceOf, not->toBeNull, …)? Happy to help investigate/contribute if it's wanted — just want to check first whether there's a known reason (e.g. the ->not chain, or higher-order testing, making this unsafe to implement in general) before looking into a PR.