ZetCode

PHP Union Types

last modified October 22, 2026

PHP union types allow a parameter, return value, or property to accept values of more than one type. They were introduced in PHP 8.0 and are written as a list of types separated by the | character, for example int|string. This tutorial covers basic and advanced usage of union types with practical examples.

Union types are useful for functions that can handle several input types or return different types depending on conditions. Before PHP 8.0, such signatures could only be described in docblock comments; now the engine checks them at runtime.

Basic union types

This example declares a parameter that can accept either an integer or a string.

basic_union.php
<?php

declare(strict_types=1);

function printValue(int|string $value): void
{
    echo $value . "\n";
}

printValue(42);
printValue("Hello");

The int|string syntax specifies that the $value parameter can be either an integer or a string. Any other type, such as an array, raises a TypeError.

$ php basic_union.php
42
Hello

Union types in return values

This example declares a function that returns either an integer or a float.

return_union.php
<?php

declare(strict_types=1);

function divide(int $a, int $b): int|float
{
    if ($a % $b === 0) {
        return intdiv($a, $b);
    }

    return $a / $b;
}

var_dump(divide(10, 2));
var_dump(divide(10, 4));

When the division is exact, the function returns an integer computed with intdiv; otherwise it returns a float. The var_dump function shows the actual type of each result.

$ php return_union.php
int(5)
float(2.5)

Union types with null

A union type can include null to indicate that a value may be missing.

nullable_union.php
<?php

declare(strict_types=1);

function findUser(int $id): string|null
{
    $users = [1 => 'John Doe', 2 => 'Jane Smith'];

    return $users[$id] ?? null;
}

echo findUser(1) ?? 'User not found', "\n";
echo findUser(3) ?? 'User not found', "\n";

The string|null return type allows the function to return either a string or null. It is equivalent to the nullable shorthand ?string. The shorthand only works with a single type; for several types, null must be written as a member of the union, as in int|string|null.

$ php nullable_union.php
John Doe
User not found

Union types in class properties

Typed properties can also use union types. Here the property is declared with constructor property promotion.

class_property_union.php
<?php

declare(strict_types=1);

class User
{
    public function __construct(public int|string $id) {}
}

$user1 = new User(1);
$user2 = new User("abc123");

var_dump($user1->id);
var_dump($user2->id);

The int|string type allows the $id property to store either an integer or a string. The property keeps the type of the value that was assigned to it.

$ php class_property_union.php
int(1)
string(6) "abc123"

Union types with arrays

This example accepts either an array or a string.

array_union.php
<?php

declare(strict_types=1);

function processInput(array|string $input): string
{
    if (is_array($input)) {
        return "Processing array: " . implode(", ", $input);
    }

    return "Processing string: " . $input;
}

echo processInput([1, 2, 3]), "\n";
echo processInput("Hello"), "\n";

The is_array check narrows the type inside the function. After the if block, the only remaining possibility is a string.

$ php array_union.php
Processing array: 1, 2, 3
Processing string: Hello

Union types with objects

Class names can be combined in a union as well.

object_union.php
<?php

declare(strict_types=1);

class Cat
{
    public function speak(): string
    {
        return "Meow";
    }
}

class Dog
{
    public function speak(): string
    {
        return "Woof";
    }
}

function makeAnimalSpeak(Cat|Dog $animal): string
{
    return $animal::class . " says " . $animal->speak();
}

echo makeAnimalSpeak(new Cat()), "\n";
echo makeAnimalSpeak(new Dog()), "\n";

The Cat|Dog type allows the parameter to accept either a Cat or a Dog object. Since both classes have a speak method, the function can call it without checking the type. In real code, a common interface is usually a better choice than a union of unrelated classes.

$ php object_union.php
Cat says Meow
Dog says Woof

Union types with numeric values

The int|float union is a common way to accept any number.

numeric_union.php
<?php

declare(strict_types=1);

function calculateSquare(int|float $number): float
{
    return $number * $number;
}

var_dump(calculateSquare(5));
var_dump(calculateSquare(2.5));

The function accepts both integers and floats. The return type is float, so the integer result 25 is converted to float(25). This int-to-float widening is allowed even in strict mode.

$ php numeric_union.php
float(25)
float(6.25)

Union types with boolean or integer

This example accepts a boolean or an integer for a configuration setting.

bool_int_union.php
<?php

declare(strict_types=1);

function setConfig(bool|int $value): string
{
    if (is_bool($value)) {
        return $value ? "Enabled" : "Disabled";
    }

    return "Level: " . $value;
}

echo setConfig(true), "\n";
echo setConfig(false), "\n";
echo setConfig(0), "\n";
echo setConfig(42), "\n";

A boolean switches the feature on or off, while an integer sets a level. The is_bool check distinguishes false from 0, which would be equal under loose comparison.

$ php bool_int_union.php
Enabled
Disabled
Level: 0
Level: 42

Handling each type with match

A match expression is a compact way to handle every member of a union.

match_type.php
<?php

declare(strict_types=1);

function describe(int|float|string|array|null $value): string
{
    return match (true) {
        is_int($value) => "integer $value",
        is_float($value) => "float $value",
        is_string($value) => "string '$value'",
        is_array($value) => "array of " . count($value) . " elements",
        $value === null => "null",
    };
}

$values = [7, 3.14, "sky", [1, 2, 3], null];

foreach ($values as $value) {
    printf("%-8s %s\n", get_debug_type($value), describe($value));
}

Each arm of match (true) tests one type. The get_debug_type function returns the type name as it would be written in a declaration, such as int or float.

$ php match_type.php
int      integer 7
float    float 3.14
string   string 'sky'
array    array of 3 elements
null     null

The false pseudo-type

Many built-in functions return a value on success and false on failure. The false type describes this pattern.

false_union.php
<?php

declare(strict_types=1);

function findPosition(string $haystack, string $needle): int|false
{
    return strpos($haystack, $needle);
}

$pos = findPosition("an old falcon", "falcon");
var_dump($pos);

$pos = findPosition("an old falcon", "eagle");
var_dump($pos);

if ($pos === false) {
    echo "eagle not found\n";
}

The int|false return type matches the behaviour of strpos. Because position 0 is a valid result, the return value must be compared with false using the === operator. Since PHP 8.2, false, true and null can also be used as standalone types.

$ php false_union.php
int(7)
bool(false)
eagle not found

TypeError for unsupported types

In strict mode, passing a value that matches none of the types in the union throws a TypeError.

type_error.php
<?php

declare(strict_types=1);

function formatId(int|string $id): string
{
    return "ID-" . $id;
}

echo formatId(15), "\n";
echo formatId("A7"), "\n";

try {
    echo formatId(3.5), "\n";
} catch (TypeError $e) {
    echo strstr($e->getMessage(), ', called', true), "\n";
}

The float 3.5 is neither an integer nor a string, so the call fails. We cut the location part from the error message to keep the output short.

$ php type_error.php
ID-15
ID-A7
formatId(): Argument #1 ($id) must be of type string|int, float given

Coercion in non-strict mode

Without declare(strict_types=1), PHP tries to convert a value that is not in the union to one of its members.

coercion.php
<?php

function show(int|float $value): void
{
    var_dump($value);
}

show("15");
show("1.5");
show("1e3");
show(true);

Numeric strings are converted to int when they hold an integer and to float otherwise. A boolean becomes an integer, because int is preferred over float.

$ php coercion.php
int(15)
float(1.5)
float(1000)
int(1)

Disjunctive normal form types

PHP 8.2 added disjunctive normal form (DNF) types, which combine union and intersection types. An intersection must be put in parentheses.

dnf_types.php
<?php

declare(strict_types=1);

interface HasName
{
    public function name(): string;
}

interface HasPrice
{
    public function price(): float;
}

class Product implements HasName, HasPrice
{
    public function __construct(private string $name, private float $price) {}

    public function name(): string
    {
        return $this->name;
    }

    public function price(): float
    {
        return $this->price;
    }
}

function label((HasName&HasPrice)|string|null $item): string
{
    if ($item === null) {
        return "no item";
    }

    if (is_string($item)) {
        return $item;
    }

    return sprintf("%s (%.2f)", $item->name(), $item->price());
}

echo label(new Product("Lamp", 24.5)), "\n";
echo label("Gift card"), "\n";
echo label(null), "\n";

The (HasName&HasPrice)|string|null type accepts an object that implements both interfaces, a string, or null.

$ php dnf_types.php
Lamp (24.50)
Gift card
no item

Rules and restrictions

Best practices for union types

Source

PHP Type System - Composite types

In this article, we have explored PHP union types, including return types, nullable and false types, coercion rules, and DNF types.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all PHP tutorials.