0

PHP 8.2 — null, true, false as standalone types

Intermediate5 min read·php-09-018

Concept

PHP 8.2 made null, true, and false usable as standalone type declarations, not just as elements inside union types. Previously, false and null appeared only within unions (int|false, string|null), and true had no standalone form at all. With PHP 8.2, you can write function terminated(): false to declare that a function always returns the boolean false, or function boot(): true to declare it always succeeds and returns true.

The motivation is precision in function signatures that wrap C extensions or system calls. PHP's own standard library uses false as a sentinel for failure in many functions: strpos() returns int|false, file_get_contents() returns string|false. Before PHP 8.2, wrapping these required union types even when your wrapper guaranteed only the failure path. Now you can express the contract exactly.

true as a standalone type is particularly useful for "this always succeeds" assertions — validation functions that throw on failure and return true on success, or factory methods that either throw or return a valid instance. It is also the idiomatic type for PHP's assert() callback parameter.

One subtlety: null as a standalone type is semantically equivalent to writing ?void in intent but void means no return value at all. A function typed null explicitly returns null. Functions that mix null with other types still use union syntax: int|null or ?int.

Standalone typeMeaningBefore 8.2 workaround
falseAlways returns falsebool or int|false
trueAlways returns truebool
nullAlways returns nullvoid (misleading) or untyped

Code Example

php
<?php
declare(strict_types=1);

// false as a standalone return type
// Models the failure-only branch of a nullable lookup
function findDeleted(int $id): false
{
    // This function is called only when we know the record was soft-deleted
    // It always returns false to signal "not available"
    return false;
}

// true as a standalone return type — validation that throws or succeeds
function assertPositive(int $value): true
{
    if ($value <= 0) {
        throw new \InvalidArgumentException("Expected positive int, got {$value}");
    }

    return true; // only reachable path that returns
}

// null as a standalone return type
// Useful for event handlers / hooks that intentionally return nothing
function onShutdown(): null
{
    // cleanup work...
    return null;
}

// Practical: wrapping strpos with precise types
function indexOfOrFail(string $haystack, string $needle): int
{
    $pos = strpos($haystack, $needle); // int|false in PHP's signature

    if ($pos === false) {
        handleMissing($needle); // returns false, never returns
    }

    return $pos;
}

function handleMissing(string $needle): false
{
    // Log the miss, return the canonical sentinel
    error_log("Needle not found: {$needle}");
    return false;
}

// Union with true/false for toggle-style returns
function toggleFeature(string $flag, bool $enable): true|false
{
    // ... toggle logic
    return $enable;
}

// Combining with match expression
function parseBoolean(string $input): true|false
{
    return match (strtolower($input)) {
        'yes', '1', 'true', 'on'  => true,
        'no',  '0', 'false', 'off' => false,
        default => throw new \ValueError("Cannot parse '{$input}' as boolean"),
    };
}

echo var_export(parseBoolean('yes'), true);  // true
echo var_export(parseBoolean('no'), true);   // false

Interview Q&A

Q: Why are true, false, and null valuable as standalone types rather than just using bool or the nullable shorthand?

Using bool when a function always returns false hides information — callers and static analyzers must handle both true and false as possibilities. A false return type tells PHPStan exactly what to expect, eliminating false positives in branches that check the return. Similarly, true removes the need for callers to guard against false after a validation function. null as a standalone type distinguishes "this function always returns null" from void (which means the return value must not be used). The precision aligns PHP's type system with the actual semantics of functions that have one-sided sentinel values, which is common in low-level utility code.


Q: How do PHPStan and Psalm narrow types when they see a false return type in a union like int|false?

When a function is typed int|false and a caller checks if ($result === false), analyzers narrow the type inside the if branch to false and outside to int. With standalone false, the function always returns false, so no narrowing is needed — the type is already exact. For union-level narrowing, int|false is the right form; false alone is for the branch where only the failure sentinel is ever returned. PHPStan 1.x and Psalm 5.x both handle the standalone forms natively in their inference engines, producing correct completions and error detection.


Q: Is true|false any different from bool at the type level?

At runtime they are identical — both accept only true and false. The difference is in static analysis semantics. bool in a return position tells analyzers "this function can return either", but it doesn't distinguish which paths yield which value. true|false is more explicit but functionally equivalent to bool. The value comes when the return is narrowed: function toggleFeature(): true|false paired with a match expression returning true in one arm and false in the other lets PHPStan verify exhaustiveness. In practice, prefer bool for general boolean returns and use true or false as standalone types only when the function is semantically locked to one side.