Union Types, Mixed, and Never Return Type
High Contrast
Dark Mode
Light Mode
Sepia
Forest
1 min read179 words

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: mixed satisfies PHPStan at level 7 and below but triggers warnings at level 8+. If you use mixed, 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');
}
}