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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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
- A type can appear only once in a union;
int|string|intis a compile-time error. voidandnevercannot be part of a union.mixedcannot be combined with other types, because it already includes all of them.bool|falseandbool|trueare redundant and rejected.- The nullable shorthand cannot be combined with a union:
?int|stringis invalid; writeint|string|null.
Best practices for union types
- Use sparingly: a long union often signals that a function does too many things. Consider an interface or separate functions.
- Handle all cases: make sure the code handles every type in
the union;
matchthrows anUnhandledMatchErrorwhen an arm is missing. - Combine with type checks: use
is_int,is_string,instanceofand similar checks to narrow the type. - Enable strict types: with
declare(strict_types=1), no implicit conversions take place, which makes the behaviour of a union easier to predict.
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
List all PHP tutorials.