Union Types, Mixed, and Never Return Type
PHP 8.0 completed the type system with union types, and PHP 8.1 added never. Together with mixed, these cover scenarios that previously required docblocks and hoping for the best.
Union Types
Union types express that a parameter or return value accepts multiple distinct types:
// PHP 8.0
function processInput(int|string $input): int|float {
if (is_string($input)) {
return strlen($input);
}
return $input * 1.5;
}
Union Types in Properties
class Payment {
public int|float $amount;
public string|null $note; // Preferred: ?string
}
Union Types vs Nullable
// These are equivalent for null:
function foo(?string $x): void {}
function foo(string|null $x): void {}
// But union types are more powerful — can combine any types:
function save(int|string|bool $value): void {}
false and null as Standalone Types
PHP 8.0 allows false and null as standalone union members:
function find(int $id): User|false {
// Returns User or false if not found
}
function getConfig(string $key): string|null {
// Returns string or null
}
mixed Type
mixed explicitly means "any type" — it's the honest declaration when a function genuinely accepts or returns anything:
// mixed = int|float|string|bool|array|object|callable|resource|null
function serialize(mixed $value): string {
return json_encode($value);
}
function deserialize(string $json): mixed {
return json_decode($json, true);
}
mixed vs no type hint
// No type hint — same as mixed but implicit (not recommended)
function bad(value): {}
// Explicit mixed — clearly communicates intent
function good(mixed $value): mixed {}
PHPStan note:
mixedsatisfies PHPStan at level 7 and below but triggers warnings at level 8+. If you usemixed, expect to narrow it with type guards.
never Return Type
never (PHP 8.1) means a function never returns — it either throws an exception or calls exit():
function abort(int $code, string $message): never {
http_response_code($code);
echo $message;
exit;
}
function throwNotFound(string $resource): never {
throw new NotFoundException("$resource not found");
}
Why never Matters for Static Analysis
Without never, PHPStan cannot know that code after abort() is unreachable:
function getUser(int $id): User {
$user = $this->repository->find($id);
if ($user === null) {
abort(404, 'User not found'); // PHPStan: might return null
}
return $user; // PHPStan: $user might be null here
}
// With never on abort():
function getUser(int $id): User {
$user = $this->repository->find($id);
if ($user === null) {
abort(404, 'User not found'); // never — PHPStan knows this exits
}
return $user; // PHPStan: $user is User here — ✅
}
Type System Overview (PHP 8.3)
PHP Type System
├── Scalar → int, float, string, bool
├── Compound → array, object, callable, iterable
├── Special → null, void, never, mixed
├── Union → int|string, string|null
├── Intersection → Countable&Iterator (PHP 8.1)
├── DNF → (Countable&Iterator)|array (PHP 8.2)
├── Nullable → ?string = string|null
├── Generics → @template T (docblock only — PHPStan/Psalm)
└── Enums → BackedEnum<string>, UnitEnum (PHP 8.1)
Practical Type Narrowing
Use type guards to narrow union types for PHPStan/Psalm compliance:
function process(int|string $value): string {
if (is_int($value)) {
// PHPStan knows $value is int here
return number_format($value);
}
// PHPStan knows $value is string here
return strtoupper($value);
}
With instanceof for objects:
function handle(Request|CliInput $input): void {
if ($input instanceof Request) {
$data = $input->getBody()->getContents();
} else {
$data = $input->getArgument('data');
}
}