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;
    }
}