PHP 8.0: Union Types
PHP has come a long way with types. We now have scalar types, return types, nullable types, and even property types in PHP 7.4!
PHP 8.0 comes with support for Union Types!
In versions prior to PHP 8.0, you could only declare a single type for properties, parameters, and return types. PHP 7.1 and newer versions have nullable types, which means you can declare the type to be null
with a type declaration similar to ?string
.
From PHP 8.0, you can declare more than one type for arguments, return types, and class properties.
class Example {
private int|float $foo;
public function squareAndAdd(float|int $bar): int|float {
return $bar ** 2 + $foo;
}
}
Similar to types enforced in previous PHP versions, PHP will now make sure the function parameters, return types, and class properties belong to one of the types declared in the definition.
Union types prior to PHP 8.0
Nullable types
PHP already has support for nullable types; You can declare a type as nullable by prefixing a ?
sign to the type. For example, if a certain property can be a string
or a null
, you can declare it as ?string
. With PHP 8.0 Union Types, string|null
will be functionally equivalent to ?string
.
iterable
type
There is also the iterable
pseudo-type that is functionally equivalent to array|Traversable
.
PHPDoc comments
PHPDoc standard had support for Union Types. You would have used a PHPDoc comment to declare the types allowed for a certain code block:
class Example {
/**
* @var int|float
*/
private $foo;
/**
* @param int|float $bar
* @return int|float
*/
public function squareAndAdd(int $bar): int {
return $bar ** 2 + $this->foo;
}
}
Union Types in PHP 8.0
From PHP 8.0, you can now declare any number of arbitrary types to for properties, arguments, and return types. There are a few exceptions that don't make sense to be used together, such as string|void
.
You can completely get rid of the @var
, @param
, and @return
PHPDoc comments now in favor of the types enforced in the code itself. This will certainly help clean the boilerplate comments that were there purely because they could not be declared in code before.
Existing nullable types are not removed, nor deprecated. You can continue to use ?string
as a shorthand to string|null
.
The iterable
type is not going to be removed either, and will be functionally equal to array|Traversable
.
The RFC further explains certain restrictions and other improvements to Union Types:
The void
type is not allowed in a Union
PHP already has support for void
type. It is only allowed as a return type because using void
anywhere else wouldn't make any sense.
Either the function must not return any value, or call return;
without specifying a value.
In PHP 8.0 Union Types, the void
type is not allowed to be combined with any other type.
For example, the following is not allowed:
function foo(): void|null {}
Special false
type
PHP core and many legacy libraries return false
to indicate negative result.
For example, if you have a function that loads use account by its ID, it might return false
to indicate that the user does not exists.
To help the adoption of union types, it is allowed to use false
as part of the union types.
function user_load(int $id): User|false {
}
The snippet above mimics Drupal's user_load()
function, where it returns a User
object if the user account is found, or false
otherwise. This special false
type can be quite helpful in this situation.
There are several PHP core functions that follow the same pattern too. For example, strpos()
function returns an int
of the position where the needle bytes were found in the string, or false
if it is not found anywhere. The special false
type can come handy here too.
false
cannot be used as a standalone type. This means a type declaration likepublic false $foo
is not allowed. You can still usebool
type for it. Since PHP 8.2, usingfalse
as a stand-alone type is allowed.- There is no
true
pseudo type prior to PHP 8.2. The aim of thefalse
pseudo type is to accommodate the (mostly legacy) code that returnsfalse
on failure. Usebool
type there. PHP 8.2, introducestrue
as a type, but it is not allowed to be used in union offalse
, i.e.false|true
is not allowed andbool
must be used. false
pseudo type is allowed anywhere types are allowed: class properties, function arguments, and return types all supportfalse
type.- *If
bool
is used,false
cannot be used in same type declaration.
Changes in PHP 8.2 There are some changes to the semantics listed above:
Nullable types (?TYPE
) and null
must not be mixed.
While ?string
is a shorthand for string|null
, the two notations must not be mixed.
If your type declaration has more than one type and null, it must be declared as the following:
TYPE_1|TYPE_2|null
It must not be declared as ?TYPE_1|TYPE_2
as this would be an ambiguous declaration.
Compile-time error checking
Union types does not allow duplicate or redundant types in type declaration. This check will happen at compile-time without autoloading classes/interfaces.
Duplicate types are not allowed.
You cannot declare int|int
or int|INT
as this essentially is the same. Furthermore, int|?int
is not allowed either.
The latter will result in a syntax error, and the former will throw this error:
Fatal error: Duplicate type ... is redundant in ... on line ...
Redundant types are not allowed.
Union types does not allow redundant class types. However, it is important to note that this only applies to a few special special types only.
bool|false
is not allowed becausefalse
is a type ofbool
.- `object cannot be used with a class name because all class objects are of type
object
too. iterable
cannot be used witharray
orTraversable
becauseiterable
is a union typearray|Traversable
already.- Class names can be used even if one extends another.
Because the Union types declarations are validated at compile-time, there will be no errors thrown if you use a parent class and a child class in union because that would require class hierarchy resolution.
Here are some examples:
function foo(): bool|false {} // Fatal error: Duplicate type false is redundant in ... on line ...
function foo(): DateTime|object {} // Fatal error: Type DateTime|object contains both object and a class type, which is redundant in ... on line ...
function foo(): iterable|array {} // Fatal error: Type iterable|array contains both iterable and array, which is redundant in ... on line ...
Because the type declaration is only checked at compile-time, the following is valid:
class A{}
class B extends A{}
function foo(): A|B {}
Type variance
Union type variance follows LSP. This is to ensure that all sub classes and implementations of the interface must not change the behavior of the program and still adhere to the contract. This is enforced in PHP even without Union types.
- Parameter are contra-variant: Types can be widened with a super type.
- Return types are covariant: Types can be restricted to a sub type.
- Property types invariant: Types cannot be changed to a sub or super type.
Adding or removing types to a Union
class A{
public function foo(string|int $foo): string|int {}
}
class B extends A{
public function foo(string|int|float $foo): string {}
}
In the snippet above, parameter types are widened (with the addition of float
type). This is allowed because all programs that use class B
expect them to accept all types class A
accepts. Class B
still fulfills this contract.
Return type in B::foo
is restricted further, but this still follows LSP because class B
fulfils the return type declared in A::foo
.
If you were to change the type in a way that class B
does not fulfill the contract, this will trigger a fatal error:
class A{
public function foo(string|int $foo): string|int {}
}
class B extends A{
public function foo(string|float $foo): string|int {}
}
Fatal error: Declaration of B::foo(string|float $foo): string|int must be compatible with A::foo(string|int $foo): string|int in ... on line ...
Variance of individual types in a Union
Same variance rules are followed when you change individual members a union type.
Subclasses
class ParentClass {}
class ChildClass extends ParentClass{}
class FooParent {
public function foo(string|ChildClass $a): string {}
public function bar(string $b): string:ParentClass {}
}
class FooChild extends FooParent{
public function foo(string|ParentClass $a): string|ChildClass {}
public function bar(string $b): string:ChildClass {}
}
This is perfectly, because in the FooChild::foo
functions parameters are expanded. All existing code that works with FooParent
class will work because ChildClass
is a sub-type of ParentClass
with same functionality.
In FooChild::bar
function, its return type is further restricted. This is also valid because FooChild::bar
still fulfills its contract by return a sub-type of string
or ChildClass
which inherits ParentClass
.
Nullable
class FooParent {
public function foo(string $a): int|null {}
}
class FooChild extends FooParent{
public function foo(string|null $b): int|null {}
}
Return types are allowed to lose nullable
type, and parameters are allowed to make they accept null
.
bool
and false
In a Union type, PHP considers false
to be a sub-type for bool
. This allows variance like this:
class FooParent {
public function foo(int|false $a): int|bool {}
}
class FooChild extends FooParent{
public function foo(int|bool $b): int|false {}
}
Related Changes
Backwards compatibility impact
Union Types is a new feature in PHP, and its new syntax does not break any existing functionality. Because this is a syntax change in the engine, this functionality cannot be brought to older versions with a poly-fill.