Conditional types
Psalm supports the equivalent of TypeScript’s conditional types.
Conditional types have the form:
(<template param> is <union type> ? <union type> : <union type>)
All conditional types must be wrapped inside brackets e.g. (...)
Conditional types are dependent on template parameters, so you can only use them in a function where template parameters are defined.
Example application
Let's suppose we want to make a userland implementation of PHP's numeric addition (but please never do this). You could type this with a conditional return type:
<?php
/**
* @template T of int|float
* @param T $a
* @param T $b
* @return int|float
* @psalm-return (T is int ? int : float)
*/
function add($a, $b) {
return $a + $b;
}
When figuring out the result of add($x, $y) Psalm tries to infer the value T for that particular call. When calling add(1, 2), T can be trivially inferred as an int. Then Psalm takes the provided conditional return type
(T is int ? int : float)
and substitutes in the known value of T, int, so that expression becomes
(int is int ? int : float)
which simplifies to (true ? int : float), which simplifies to int.
Calling add(1, 2.1) means T would instead be inferred as int|float, which means the expression (T is int ? int : float) would instead have the substitution
(int|float is int ? int : float)
The union int|float is clearly not an int, so the expression is simplified to (false ? int : float), which simplifies to float.
Nested conditionals
You can also nest conditionals just as you could ternary expressions:
<?php
class A {
const TYPE_STRING = 0;
const TYPE_INT = 1;
/**
* @template T of int
* @param T $i
* @psalm-return (
* T is self::TYPE_STRING
* ? string
* : (T is self::TYPE_INT ? int : bool)
* )
*/
public static function getDifferentType(int $i) {
if ($i === self::TYPE_STRING) {
return "hello";
}
if ($i === self::TYPE_INT) {
return 5;
}
return true;
}
}