Skip to content

[Bug]: @phpstan-type alias used in an @extends generic argument is read as a class name in parent methods #111

Closed

Description

@fdaviddpt

Summary

A subclass defines a type alias with @phpstan-type and uses it in the generic argument of its @extends tag. When a method declared in the parent returns the template, TypePHP checks the value against a class literally named after the alias instead of the alias's shape:

App\TotalsQuery::fetch(): Return value[0] must be of type Row, associative array (key 'user_id') returned

Row is array{user_id: int, total: float}, and the returned row matches it exactly. PHPStan (level 9) accepts the code.

Playground

Open in playground — output: run: App\TotalsQuery::fetch(): Return value[0] must be of type Row, associative array (key 'user_id') returned

<?php

declare(strict_types=1);

namespace App;

/**
 * @template TOutput of array
 */
abstract class Query
{
    /**
     * @return TOutput
     */
    public function run(): array
    {
        return $this->fetch();
    }

    /**
     * @return TOutput
     */
    abstract protected function fetch(): array;
}

/**
 * @phpstan-type Row array{user_id: int, total: float}
 * @extends Query<list<Row>>
 */
final class TotalsQuery extends Query
{
    protected function fetch(): array
    {
        return [['user_id' => 1, 'total' => 2.5]];
    }
}

try {
    (new TotalsQuery())->run();
    echo "run: OK\n";
} catch (\TypeError $e) {
    echo "run: ", $e->getMessage(), "\n";
}

Actual

run: App\TotalsQuery::fetch(): Return value[0] must be of type Row, associative array (key 'user_id') returned

Expected

run: OK

Control

Give the child's fetch() its own @return list<Row>: fetch() then passes, but run() (inherited, @return TOutput) still fails with the same message. So the alias resolves fine in the subclass's own docblocks, and is lost only when it travels through the template binding into a parent method. It looks like the type-alias counterpart of #93 (relative class name in a generic argument): the alias should be resolved in the scope of the class that wrote the @extends tag.

Versions

v0.11.3 and dev-main @ 4e4244e (local project, PHP 8.3, array_validation => 'full'), and the playground. PHPStan 2.3.0 level 9: no error.

Context

This is how our query commands type their results: the base class owns run()/manageQuery() returning TOutput, each concrete command declares its row shape as a local @phpstan-type and binds it in @extends. Every such command reports one violation per returned row.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions