Psalm: Templated types do not carry over input complex types

Created on 6 Apr 2020  路  3Comments  路  Source: vimeo/psalm

Context: I'm currently trying to implement a library that allows type-safe decoration of arbitrary data structures via interfaces. The idea is to do something like:

function decorate(
    object $input,
    string $addedInterface,
    callable $methodDefinition
): object {}

To achieve type-safety on the above, I would need to add $addedInterface and $input to the types of the return value, but psalm doesn't seem to consider input type definitions when using complex types.

Given following snippet (https://psalm.dev/r/29133cb42f):

<?php

interface Input {}
interface HasFoo { function foo() : int; }
interface HasBar { function bar() : string; }

/**
 * @psalm-template InputType of Input
 * @psalm-param InputType $input
 * @psalm-return InputType&HasFoo
 */
function decorateWithFoo(Input $input): Input
{
    throw new \BadMethodCallException('not implemented for ' . get_class($input));
}

/**
 * @psalm-template InputType of Input
 * @psalm-param InputType $input
 * @psalm-return InputType&HasBar
 */
function decorateWithBar(Input $input): Input
{
    throw new \BadMethodCallException('not implemented for ' . get_class($input));
}

/** @param HasFoo&HasBar $input */
function useFooAndBar(object $input): string
{
    return 'foo: ' . $input->foo()
        . ' bar: ' . $input->bar();
}

function consume(Input $input): void
{
    echo useFooAndBar(decorateWithFoo(decorateWithBar($input)));
}

Psalm reports:

Psalm output (using commit 95bc960): 

ERROR: InvalidArgument - 36:23 - Argument 1 of useFooAndBar expects HasFoo&HasBar, Input&HasFoo provided

The expectation in this case is that the call to useFooAndBar() is correctly inferred with HasFoo&HasBar as input value.

bug

Most helpful comment

well that was fast :O

All 3 comments

I found these snippets:


https://psalm.dev/r/29133cb42f

<?php

interface Input {}
interface HasFoo { function foo() : int; }
interface HasBar { function bar() : string; }

/**
 * @psalm-template InputType of Input
 * @psalm-param InputType $input
 * @psalm-return InputType&HasFoo
 */
function decorateWithFoo(Input $input): Input
{
    throw new \BadMethodCallException('not implemented for ' . get_class($input));
}

/**
 * @psalm-template InputType of Input
 * @psalm-param InputType $input
 * @psalm-return InputType&HasBar
 */
function decorateWithBar(Input $input): Input
{
    throw new \BadMethodCallException('not implemented for ' . get_class($input));
}

/** @param HasFoo&HasBar $input */
function useFooAndBar(object $input): string
{
    return 'foo: ' . $input->foo()
        . ' bar: ' . $input->bar();
}

function consume(Input $input): void
{
    echo useFooAndBar(decorateWithFoo(decorateWithBar($input)));
}
Psalm output (using commit 95bc960):

ERROR: InvalidArgument - 36:23 - Argument 1 of useFooAndBar expects HasFoo&HasBar, Input&HasFoo provided

Use-cases for this would be:

  • HTTP validation layers
  • filtering layers
  • PSR-15 middleware applications
  • CLI application input validation layer

Any kind of checked upcast could become an &HasFoo, solving potentially a gazillion of issues :-)

well that was fast :O

Was this page helpful?
0 / 5 - 0 ratings