Lesson 68 of 100
Types15 min5 checksPHP 8.5

Model Alternatives with Union Types

Learn how PHP 8.4 union types describe values that may be one of several types, how coercion picks a type, and how to narrow a union safely inside a function.

01 · THE IDEAREAD → TRY → LEARN

Why union types exist

Real inputs are often not one clean type. An identifier might arrive as the integer 42 or the string "42". A form field might be a string or missing entirely. Before PHP 8.0 you either left the parameter untyped or documented the alternatives in a comment. A union type lets the declaration itself say which types are acceptable.

You write a union by joining types with a vertical bar, for example int|string. PHP checks the argument when the function is called and throws a TypeError if the value does not fit. Union types were added in PHP 8.0 and work for parameters, return values and typed properties.

Nullable types are unions

The short form ?string means exactly the same as string|null. Once you have more than one other type, use the long form: int|string|null. Since PHP 8.2, null and false may also stand alone as types, but for everyday work the union with null is the common pattern.

Declaring a parameter as int $x = null is an older way to make it nullable. It is deprecated in PHP 8.4, so write ?int $x = null or int|null $x = null instead.

Coercion: which member wins?

In the default coercive mode, PHP may convert a scalar to fit the declaration. With a union, it has to choose one member. An exact match is always kept: an int passed to int|string stays an int. If there is no exact match, PHP prefers int, then float, then string, then bool. A float with no fractional part can become an int. A fractional float such as 4.5 is also converted to int for this union in coercive mode, but the loss of precision raises a deprecation notice on PHP 8.4. It does not fall back to string. The runnable example uses integral floats to avoid that deprecation.

Example
<?php
function show(int|string $value): string {
    return get_debug_type($value);
}

echo json_encode([show(3.0), show(4.0), show(true), show("8")], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
Output
["int","int","int","string"]

This surprises people, so be deliberate about it. If you do not want coercion, enable strict typing.

Strict mode

With declare(strict_types=1); at the top of the calling file, scalar values must match a member of the union exactly. The only exception is that an int is accepted where float is declared. A float passed to int|string then throws a TypeError.

Example
<?php
declare(strict_types=1);

function show(int|string $value): string {
    return get_debug_type($value);
}

try {
    echo show(3.0), "\n";
} catch (TypeError $e) {
    echo "TypeError\n";
}
echo show(3), "\n";
Output
TypeError
int

Strict mode depends on the file that makes the call, not on the file that defines the function.

Narrowing inside the function

A union only describes what may come in. Inside the function you still have to find out which member you actually received, and then handle each case. Use === null, is_int() and is_string() for this. After you rule out some members, the remaining possibilities are easier to reason about.

Example
<?php
function labelFor(int|string|null $value): string {
    if ($value === null) {
        return 'none';
    }
    if (is_int($value)) {
        return 'number:' . $value;
    }
    return 'text:' . trim($value);
}

echo json_encode([labelFor(null), labelFor(9), labelFor(' ab ')], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
Output
["none","number:9","text:ab"]

After the null and is_int() checks, only string is left, so trim() is safe to call.

Rules to remember

  • Each type may appear only once. int|string|INT is a compile-time error.
  • You cannot use mixed or never inside a union.
  • If you use bool, do not also list true or false.
  • Do not use boolean or integer as type names. PHP treats them as class names, not as bool and int.
  • Keep unions small. If a function needs five alternatives and a branch for each, it probably does too much.

Exercise contract

Write normalizeQuantity(int|string|null $value): int. It must turn a quantity that may arrive in several shapes into a non-negative integer:

  • null returns 0.
  • An integer returns itself, but negative integers return 0.
  • A string is trimmed first. If the result contains only digits, return it as an integer. Anything else, including an empty string, returns 0.

Use ctype_digit() on the trimmed string rather than a bare (int) cast. A cast would turn "4x" into 4, which hides bad input. The tests compare the returned values, so do not print anything.

Recap

Union types name the alternatives a value may have. Coercive mode picks a member by a fixed preference, and strict mode demands an exact match. Narrow the value inside the function with explicit checks, and handle each member deliberately.

Before you start

Functions · Arrays · Types

You’ll practice
  • Handle multiple allowed scalar input types explicitly
  • Implement and test a deterministic PHP 8.4 function
Your task

Make normalizeQuantity() pass all 5 checks. Run your code as often as you like, then check your answer.

exercise.phpSaved in this browserPHP 8.5
CONSOLE & CHECKS● Ready

Ready when you are

Your output will appear here.
Make it work. Then make it pass.

PHP loads on your first run. Each check starts fresh. Your code stays in this browser; clearing site data removes your progress. A check or the task looks wrong? Report a problem with this exercise.