phpphpstormpsalm-php

Psalm annotation for multiple-type template


I need to build a trait (or class for that matter) on which I can template multiple types; I've tried something like the following (also descriptive of the problem; the car context is just for illustrating the problem, I know a car is supposed to be aggregated not composed but this is not the issue to discuss):

/**
  * @template TyreType of Tyre
  * @template EngineType of Engine
  */
trait Car {
    /**
      * @return TyreType
      */
    public function getTyre(): Tyre {
    }

    /**
      * @return EngineType
      */
    public function getEngine(): Engine{
    }
}

trait SomeCar {
    /**
     * @use Car<AirlessTyre><DieselEngine>
     */
    use Car;

    public function test() {
        $this->getEngine()->dieselSpecificMethod();
    }
}

class Engine{}
class Tyre{}
class DieselEngine extends Engine {
    public function dieselSpecificMethod() {}
}
class AirlessTyre extends Tyre {}

The problem is, in PhpStorm I get "Potentially polymorphic call. Engine does not have members in its hierarchy" on dieselSpecificMethod().

So my questions are:


Solution

  • Psalm does support multiple type parameters. The correct syntax to use them is GenericType<TA, TB> (GenericType<TA><TB> that you used is not recognized). With that problem fixed (and a couple of more suppressed, to get rid of unnecessary noise) this becomes:

    <?php
    /**
      * @template TyreType of Tyre
      * @template EngineType of Engine
      */
    trait Car {
        /**
          * @return TyreType
          * @psalm-suppress InvalidReturnType
          */
        public function getTyre(): Tyre {
        }
    
        /**
          * @return EngineType
          * @psalm-suppress InvalidReturnType
          */
        public function getEngine(): Engine{
        }
    }
    
    trait SomeCar {
        /**
         * @use Car<AirlessTyre,DieselEngine>
         */
        use Car;
    
        public function test():void {
            $this->getEngine()->dieselSpecificMethod();
            $this->getEngine()->warp(9);
        }
    }
    
    class FordCar { use SomeCar; }
    
    class Engine{}
    class Tyre{}
    class DieselEngine extends Engine {
        public function dieselSpecificMethod():void {}
    }
    class WarpEngine extends Engine {
        public function warp(int $speed): void {}
    }
    class AirlessTyre extends Tyre {}
    

    Psalm can tell you Ford certainly doesn't have warp engine capabilities: https://psalm.dev/r/31343aafc3. Note the correct syntax to reference generic trait in @use annotation.