-
Notifications
You must be signed in to change notification settings - Fork 668
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Inconsistent generic beheviour on optional generics #8487
Comments
I found these snippets: https://psalm.dev/r/f86eb62c76<?php
/**
* @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');
}
|
There's definitely something weird going on here if it's sometimes |
Mixed is not the correct type here. It should be I even noticed today that the named constructor doesn't consistently results in never |
Hmm, maybe you're right. I don't think this use case was accounted for when templates were implemented though, so I'm not terribly surprised that it doesn't work. |
I can imagine :) They fall back to the Let me know if there is anything else I can do here to make your life easier. |
Faced a similar issue here: |
I found these snippets: https://psalm.dev/r/765fa2e560<?php
declare(strict_types=1);
/**
* @template-covariant T
*/
class Ok
{
/** @param T $v */
public function __construct(
public mixed $v
) {
}
}
class Err
{
public function __construct(
public string $e
) {
}
}
/**
* @template-covariant V
*/
class Result
{
/** @param Ok<V>|Err $val */
private function __construct(
public Ok|Err $val
) {
}
/**
* @template OkType
*
* @param OkType $item
*
* @return self<OkType>
*/
public static function ok($item): self
{
return new self(new Ok($item));
}
/**
* @return self<never>
*/
public static function err(string $error): self
{
return new self(new Err($error));
}
}
/**
* @template-covariant T
*/
class Decoder
{
/**
* @param \Closure(mixed): Result<T> $fn
*/
public function __construct(
public Closure $fn
) {
}
/**
* @psalm-return self<bool>
*/
public static function bool(): self
{
return new self(
function (mixed $value): Result {
if (!is_bool($value)) {
return Result::err('Not a bool value');
}
return Result::ok($value);
}
);
}
}
|
In case someone want to play around with this, I made a faling test-suite on how I would see the inferred results here: |
Having the possibility to set default types on templates could be the solution for these kind of issues: E.g. PHPStan: |
This code best describes it:
https://psalm.dev/r/f86eb62c76
I noticed this error when wrapping in yet another layer which applies conditional love to this config.
Something along the lines of
It never resolves to Foo, since it is not empty.
If I change it to:
ConfigA is mixed
it always resolves to FooThe text was updated successfully, but these errors were encountered: