Psalm types inferring for abstract classes and its implementations

79 views Asked by At

Is something wrong with templates in this example: https://psalm.dev/r/113297eeaf?

Why Psalm doesn't agree that Pet<Cat|Dog> and Cat|Dog are the same types here? Can this be solved somehow (besides baseline or suppression)?

<?php

/**
 * @template T
 */
abstract class Animal
{
}

/**
 * @template T of Cat|Dog
 * @extends Animal<T> 
 */
abstract class Pet extends Animal
{
    abstract public function say(): string;
}


/**
 * @extends Pet<Cat> 
 */
class Cat extends Pet
{
    public function say(): string
    {
        return 'meow';
    }
}


/**
 * @extends Pet<Dog> 
 */
class Dog extends Pet
{
    public function say(): string
    {
        return 'woof';
    }
}

function someFunction(Pet $pet): void
{
    echo $pet->say();
}

$pet = rand(0,1) === 0 
    ? new Dog()
    : new Cat()
;
someFunction($pet);

ERROR: InvalidArgument - 52:14 - Argument 1 of someFunction expects Pet<Cat|Dog>, but Cat|Dog provided

1

There are 1 answers

0
Biapy On BEST ANSWER

Using generic typing is not useful in your example. extends is sufficient. A Cat is a Pet. A Dog is a Pet.

Generic typing is useful to ensure a given function, method, or class returns (and/or accepts) the wanted type.

For example a PetHouse can host either a Cat or a Dog, but a PetHouse<Cat> can only host (and return) a Cat.

<?php

/**
 * An animal
 */
abstract class Animal
{
}

/**
 * @extends Animal
 */
abstract class Pet extends Animal
{
    abstract public function say(): string;
}

/**
 * @extends Pet
 */
class Cat extends Pet
{
    public function say(): string
    {
        return 'nya';
    }

    public function meow(): string
    {
        return $this->say();
    }
}


/**
 * @extends Pet
 */
class Dog extends Pet
{
    public function say(): string
    {
        return 'woof';
    }

    public function bark(): string
    {
        return $this->say();
    }
}

/**
 * A Pet house
 * 
 * @template T of Pet
 */
class PetHouse {
  /**
   * @param T $pet the pet
   */
  public function __construct(
    protected Pet $pet
  ) {
  }

  /**
   * @return T
   */
  public function getPet(): Pet {
    return $this->pet
  }
}


function someFunction(Pet $pet): void
{
    echo $pet->say();
}

/**
 * @param PetHouse<Cat>
 */
function someOtherFunction(PetHouse $house): void
{
    echo $house->getPet()->meow();
}


$pet = rand(0,1) === 0 
    ? new Dog()
    : new Cat()
;
someFunction($pet);

/** @var PetHouse<Cat> $catHouse */
$catHouse = new PetHouse(new Cat());
someOtherFunction($catHouse);