Psalm: Generic collection with more specific type in annotation.

Created on 24 May 2019  Â·  7Comments  Â·  Source: vimeo/psalm

Given the following example, shouldn't Psalm complain about an invalid argument BaseballPlayer?

abstract class Player
{
}

final class FootballPlayer extends Player
{
}

final class BaseballPlayer extends Player
{
}

/**
 * @template T of Player
 */
final class Team
{
    /**
     * @var array<T>
     */
    private $players = [];

    /**
     * @param array<T> $players
     */
    public function __construct(array $players)
    {
        $this->players = $players;
    }
}

/**
 * @var Team<FootballPlayer>
 */
$team = new Team(
    [
        new FootballPlayer,
        new BaseballPlayer,
    ]
);

Most helpful comment

But when a function expects the generic version Team<Player> it will also complain.

That's due to template invariance - you can fix that by changing @template T of Player to @template-covariant T of Player

So like a @psalm-check annotation?

All 7 comments

Psalm uses @var as a type override for the assignment expression here. It doesn't check type compatibility for assignments at all:

  /** @var int $i */
  $i = 1;
  $i = "a string"; // it's fine, $i has no fixed type

Basically, assignment makes $team var to assume Team<FootballPlayer|BaseballPlayer> type, and then you're overriding it to be Team<FootballPlayer> with @var.

Thank you for the quick reply!
Is there currently a way to achieve something comparable? Because it feels like there should but I'm just missing something :-D.

No easy way (that wouldn't require changes to the actual code) that I know of. Function return type is checked for compatibility with the type of return value, so theoretically you could wrap it into a closure:

$team = 
/** @return Team<FootballPlayer> */
(function() {
   return  new Team(
    [
        new FootballPlayer,
        new BaseballPlayer,
    ]
  );
})();

This is too ugly (not to mention performance hit) though.

@muglug What do you think about introducing annotation that would ensure expression type?

@basbl is there a problem if you omit the @var? Psalm should infer it, and warn you when passing to functions that expect Team<FootballPlayer>

No problem when I omit the @var annotation and indeed when passed to a function that expects Team<FootballPlayer> Psalm will infer the type correctly and complain when given a mixed version.

But when a function expects the generic version Team<Player> it will also complain.

$team = new Team(
    [
        new FootballPlayer,
        new FootballPlayer,
    ]
);

/**
 * @param Team<Player> $team
 */
function allTeamsAllowed(Team $team): Team 
{
    return $team;
}

allTeamsAllowed($team);

Output:

➜ vendor/bin/psalm src/
Scanning files...
Analyzing files...

ERROR: InvalidArgument - src/test.php:54:17 - Argument 1 of allTeamsAllowed expects Team<Player>, Team<FootballPlayer> provided
allTeamsAllowed($team

@weirdan 's suggestion would be a massive help and tide us over until PHP gets actual support for Generics. And allow better reuse of code without any more specific versions.

But when a function expects the generic version Team<Player> it will also complain.

That's due to template invariance - you can fix that by changing @template T of Player to @template-covariant T of Player

So like a @psalm-check annotation?

Thanks @muglug ! @template-covariant was exactly what I was looking for! But instead of @template-covariant T of Player it should be @template-covariant T extends Player.

Something along the way of @psalm-check would be awesome!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

albe picture albe  Â·  3Comments

orklah picture orklah  Â·  3Comments

zerkms picture zerkms  Â·  3Comments

vudaltsov picture vudaltsov  Â·  3Comments

vudaltsov picture vudaltsov  Â·  3Comments