Skip to content

Inaccurate type inference for generic return value #11138

@westonruter

Description

@westonruter

Bug report

I'm attempting to implement a class that has caching for the method's return values.

For example, given a class Cat that has three methods:

  • count_descendants(): int
  • is_hungry(): bool
  • get_name(): string

I have a private $result_cache class member variable which is typed as follows:

 * @phpstan-type ResultCache array{
 *                               count_descendants?: int<1, max>,
 *                               is_hungry?: bool,
 *                               name?: non-empty-string
 *                           }

There is a get_cached_result method which is used as follows:

	public function count_descendants(): int {
		return $this->get_cached_result( __FUNCTION__, static function () {
			return 123; // TODO: Some expensive operation.
		} );
	}

And finally the get_cached_result method itself is implemented as follows:

	/**
	 * Get cached result.
	 *
	 * @template K of key-of<ResultCache>
	 * @phpstan-param K                         $method   Method.
	 * @phpstan-param Closure(): ResultCache[K] $callback Callback.
	 * @phpstan-return ResultCache[K]
	 *
	 * @param string  $method   Method name.
	 * @param Closure $callback Callback to get the result.
	 * @return mixed
	 */
	private function get_cached_result( string $method, Closure $callback ) {
		if ( array_key_exists( $method, $this->result_cache ) ) {
			return $this->result_cache[ $method ];
		}
		$result = $callback();
		$this->result_cache[ $method ] = $result;
		return $result;
	}

The problem is on this line:

$this->result_cache[ $method ] = $result;

PHPStan is flagging this as an error saying that $result is:

array{
    count_descendants?: bool|int<1, max>|non-empty-string,
    is_hungry?:         bool|int<1, max>|non-empty-string, 
    name?:              bool|int<1, max>|non-empty-string
}

Which does not match the type of $result_cache which is (as shown above):

array{
    count_descendants?: int<1, max>,
    is_hungry?:         bool,
    name?:              non-empty-string
}

What seems to be happening is PHPStan thinks the type of $result is a union of all possible types for the array values in the ResultCache type. But this seems like it should not be because I specified that the supplied closure returns the type ResultCache[K] where K is one of key-of<ResultCache>.

Is this a bug in PHPStan? Or am I just doing generics wrong?

Code snippet that reproduces the problem

https://phpstan.org/r/b203ec8c-2d70-469c-b24f-9e7143cff72d

Expected output

I believe this is a false positive. No error should have been detected.

Did PHPStan help you today? Did it make you happy in any way?

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions