Search code examples
phppsalm-php

Psalm types inferring for abstract classes and its implementations


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


Solution

  • 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);