Skip to content

Inconsistent generic beheviour on optional generics #8487

@veewee

Description

@veewee

This code best describes it:

https://psalm.dev/r/f86eb62c76

/**
 * @template A
 */
class Config {
    /**
     * @param (callable(): A)|null $x
     */
    public function __construct($x = null)
    {}

    /**
     * @template NewA
     * @param (callable(): NewA)|null $x
     * @return Config<NewA>
     */
    public static function create($x = null)
    {
        return new self($x);
    }
}

// Named constructor Tests:
/** @return Config<never>*/
function namedEmptyConfig() {
    return Config::create();
}

/**
 * This somehow resolves to mixed instead of never - even though the value is also null here:
 * @return Config<never>
 */
function namedNullConfig() {
    return Config::create(null);
}


// Test config constructor:
/**
 * @return Config<never>
 * Why does this work differently than the named constructor?
 * I would expect "never" to be the result here, but instead it is mixed.
 */
function emptyConfig() {
    return new Config();
}

/**
 * @return Config<never>
 * Same issue as in named constructor test - I'dd expect never here.
 */
function nullConfig() {
    return new Config(null);
}

/** @return Config<string> */
function configured() {
    return new Config(fn () => 'hello');
}

I noticed this error when wrapping in yet another layer which applies conditional love to this config.
Something along the lines of

@return ConfigA is empty ? Foo : Bar

It never resolves to Foo, since it is not empty.
If I change it to: ConfigA is mixed it always resolves to Foo

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions