PHP Type System

PHP went from "dynamic Wild West" to having one of the richest type systems of any dynamic language. Master strict types, unions, intersections, DNF, and the variance rules that govern them.

Advanced 10 min read 11 examples

How PHP Got Types

VersionAdded
5.0Class type hints for parameters
5.1array type hint
7.0Scalar types, return types, strict_types
7.1Nullable types (?T), void
7.2object type
7.4Typed properties
8.0Union types, mixed, static return, false type
8.1Intersection types, never, readonly properties
8.2true/false/null standalone, DNF types, readonly classes
8.3Typed class constants

strict_types Declaration

PHPfile.php
<?php
declare(strict_types=1);   // MUST be the first statement

function add(int $a, int $b): int {
    return $a + $b;
}

add(1, 2);          // 3
add("1", "2");      // TypeError - strings rejected

Without strict_types, PHP coerces: add("1", "2") would return 3. With strict mode, mismatches throw TypeError - bugs surface immediately.

Strict types is per-file

The declaration affects only the file it appears in - specifically, calls FROM that file to typed functions. Add it to every new file you write. Tools like PHPStan can enforce its presence.

Scalar Type Declarations

TypeAccepts
intIntegers
floatFloats (also accepts int)
stringStrings
booltrue / false
arrayAny array
objectAny object
iterablearray or Traversable
callableFunction, closure, [obj, method]
ClassName / InterfaceNameInstances of those
selfSame class
staticLate-static-bound class
parentParent class

Nullable Types

PHP
<?php
// Two equivalent ways
function findUser(?int $id): ?User { /* ... */ }
function findUser(int|null $id): User|null { /* ... */ }

// Default null forces nullable
function f(int $x = null) {}    // implicit: ?int  (deprecated in 8.4 - use explicit)
function f(?int $x = null) {}   // explicit - good

Union Types (PHP 8.0)

PHP
<?php
function format(int|string $value): string {
    return (string) $value;
}

function read(string|resource $source): string {
    if (is_string($source)) return file_get_contents($source);
    return stream_get_contents($source);
}

// "string|false" is the idiomatic way to express "or failure"
function findEmail(int $id): string|false {
    return $repo->getEmail($id) ?? false;
}

Intersection Types (PHP 8.1)

The parameter must satisfy all listed types:

PHP
<?php
// Must be both iterable AND countable
function process(Iterator&Countable $collection): void {
    echo count($collection);
    foreach ($collection as $item) { /* ... */ }
}

// Useful when you need multiple capabilities
function sign(MessageInterface&Stringable $message): string {
    return hash("sha256", (string) $message);
}

Disjunctive Normal Form (DNF) Types (PHP 8.2)

Union of intersections - the missing piece for expressing complex constraints:

PHP
<?php
function handle((Iterator&Countable) | array $data): void {
    // Accepts:
    //   - an object that is both Iterator AND Countable
    //   - OR a plain array
}

// Each intersection is parenthesised
function example(
    (HasId&HasName) | (HasUuid&HasSlug) | null $entity
): void {}

Special Types

TypeMeaning
voidFunction returns nothing (cannot return $value)
neverFunction never returns (throws or exits)
mixedAny value (use sparingly - usually means "I haven\'t thought about types")
true / false / nullLiteral types (8.2+) - e.g., string|false
selfThe current class
staticLate-static-bound class (use as return type for fluent APIs)
PHP
<?php
function abort(string $msg): never {
    throw new RuntimeException($msg);
    // No return possible
}

class QueryBuilder {
    public function where(string $col, mixed $val): static {
        // returning static lets subclasses chain correctly
        return $this;
    }
}

Typed Properties (PHP 7.4+)

PHP
<?php
class User {
    public int $id;
    public string $name;
    public ?string $bio = null;
    public DateTimeImmutable $createdAt;
}

$u = new User();
$u->name = "Ruban";
echo $u->createdAt->format("Y-m-d");
// Error: Typed property User::$createdAt must not be accessed before initialization

Typed properties are uninitialized until set - accessing one before assignment throws. Either provide a default, set in the constructor, or use nullable.

Variance Rules

When overriding methods, you can:

  • Widen parameter types (contravariance) - accept more than the parent did
  • Narrow return types (covariance) - return more specific than the parent
PHP
<?php
class Animal {}
class Dog extends Animal {}

class Pet {
    public function adopt(Dog $d): Animal { /* ... */ }
}

class Shelter extends Pet {
    // OK - param widened, return narrowed
    public function adopt(Animal $a): Dog { /* ... */ }
}

// The reverse would be unsafe - callers expecting Pet contract would break

Next Steps

Frequently Asked Questions

Practically no. It changes how type mismatches are handled (throw vs coerce), not how typed code runs. The performance gain comes indirectly: stricter types let the engine optimise more aggressively (especially with JIT).

Yes - it pays back many times over in IDE autocomplete, static analysis (PHPStan/Psalm), and catching bugs. The only exception is when a function genuinely accepts mixed (rare).

They're equivalent. ?string is shorthand for string|null. Use ?Type for single-type-plus-null; switch to X|Y|null when you need a real union.

Intersections currently support only interfaces and class types in PHP 8.1+. You can't intersect scalars (string&int makes no sense - no value satisfies both).

The function never returns - it either throws or calls exit/die. The engine and static analysers use this for unreachable-code analysis. Useful for assertion helpers and abort functions.