Как вы наверняка знаете, в Psalm и PHPStan есть типы
non-empty-string, non-empty-list<TValue> и non-empty-array<TKey, TValue>. В далёкой 0.2 версии тайфуна они обрабатывались индивидуально — у каждого был свой метод в TypeVisitor. Затем в 0.3 мы попытались сократить избыточность, внедрив конструктор non-empty<T>, но не покидало ощущение, что это костыль. И, наконец, месяц назад пришло осознание, что нужен конструктор не для непустоты, а для отрицания.В самом деле, выведем статически типы внутри вот такой функции:
/**
* @template T
* @param T $bar
*/
function foo(mixed $bar): void
{
if (is_string($bar)) {
// тут у $bar тип T & string
return;
}
// а тут у $bar тип T & !string
trim($bar); // ошибка: ожидается string
}
Очевидно, что для статического анализатора отрицание может быть очень полезно. В TypeScript был такой PR, но почему-то заглох. Psalm и PHPStan частично поддерживают
! в аннотациях @assert:
/**
* @psalm-assert !null $value
* @phpstan-assert !null $value
*/
function assertNotNull($value): void
{
if ($value === null) {
throw new InvalidArgumentException();
}
}
Ну а в typhoon/type 0.4
not теперь first-class тип. Семейство non-empty-*, а заодно и non-falsy-string выражаются так:
non-empty-string = string & !''
non-falsy-string = truthy-string = non-empty-string & !'0'
non-empty-list<TValue> = list<TValue> & !array{}
non-empty-array<TKey, TValue> = array<TKey, TValue> & !array{}
Здесь array{} — это запечатанный array-shape без элементов, то есть []. Его ещё часто пишут как array<never, never>.
Чтобы обойти
types::nonEmptyString через TypeVisitor, достаточно смаршрутизировать его как пересечение с соответствующими аргументами:
enum types implements Type
{
// ...
case nonEmptyString;
public function accept(TypeVisitor $visitor): mixed
{
return match ($this) {
// ...
self::nonEmptyString => $visitor->intersection($this, [
self::string,
self::not(self::string('')),
]),
}
}
}