Types and the Type System

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against PHP 8.5, checked on 2026-10-05; the version in which each type or rule appeared is noted, so code that must run on PHP 8.3 or 8.4 can be checked against it. It describes the values PHP knows about and the type declarations that let you state, and have the engine enforce, which values a parameter, return value, property or constant may hold. Conversions between types (juggling, casting, comparisons) are the subject of Type Juggling and Comparisons.

The type system at a glance

PHP is dynamically typed: every value carries its type at runtime, and a variable can hold values of different types over its life. It is also gradually typed: declarations are optional, and where present they are checked at runtime, throwing a TypeError on mismatch. The manual describes it as a nominal type system (types are related by their declared names, class B extends A, not by their structure) with a behavioural subtyping relation checked at compile time, while the types of actual values are verified at run time.

The building blocks are:

  • Atomic types:

    • built-in: the scalar types bool, int, float and string; array; object; resource; never; void; the relative class types self, parent and static; the singleton (literal) types false and true; the unit type null;

    • user-defined types, also called class-types: classes, interfaces and enumerations;

    • callable.

  • Composite types: union types (T|U), intersection types of class-types (T&U) and their combination in disjunctive normal form (DNF, (T&U)|V).

  • Type aliases: mixed (any value: object|resource|array|string|float|int|bool|null) and iterable (Traversable|array). PHP has no user-defined type aliases; static analysers provide them through PHPDoc.

Diagram of PHP’s type lattice. The top type mixed has as subtypes scalar (int, float, string and bool, whose subtypes are true and false), array, object (whose subtypes are the class-types: classes, interfaces, enums, self, parent and static), resource and null. The bottom type never is a subtype of every type. A side box lists the aliases iterable, equal to array or Traversable, and callable. void stands outside the lattice as a return-only type. A legend shows union, nullable, intersection and DNF composition.

get_debug_type() (PHP 8.0) returns a value’s type using the names you would write in a declaration, which makes it the right function for error messages; the older gettype() returns historical names such as "integer", "double" and "NULL":

<?php

declare(strict_types=1);

enum Format { case Ebook; }

$values = [null, true, 42, 3.14, 'Solaris', [1, 2], new stdClass(), Format::Ebook, fopen('php://memory', 'r'), strlen(...)];

foreach ($values as $value) {
    printf("%-22s %s\n", get_debug_type($value), gettype($value));
}
null                   NULL
bool                   boolean
int                    integer
float                  double
string                 string
array                  array
stdClass               object
Format                 object
resource (stream)      resource
Closure                object

The types, one by one

null

null is the only value of the null type. A variable is null if it was assigned null, has not been set yet (an undefined variable, which also raises a warning when read) or was unset(). The constant is case-insensitive (NULL, null), but lower case is the convention. is_null($x) is the same as $x === null.

<?php

declare(strict_types=1);

$returnedAt = null;
var_dump($returnedAt === null);   // bool(true)
var_dump(isset($returnedAt));     // bool(false) -- isset() is false for null

function findTitle(array $byIsbn, string $isbn): ?string
{
    return $byIsbn[$isbn] ?? null;   // null means "not found"
}

var_dump(findTitle(['978-0-00-000001-1' => 'Kindred'], '000'));   // NULL

Since PHP 8.2 null can be declared on its own (useful in a subclass that narrows a return type) and has always been usable in unions as T|null or ?T.

Source: NULL

bool

bool has two values, true and false (case-insensitive constants). When another type is used in a boolean context, these values are considered false: false, 0, 0.0, -0.0, "", "0", [], null and SimpleXML objects created from empty elements without attributes. Everything else is true, including "0.0", " ", "false" and every object.

<?php

declare(strict_types=1);

$candidates = [
    '0' => 0, '0.0' => 0.0, "''" => '', "'0'" => '0', "'0.0'" => '0.0', "' '" => ' ',
    "'false'" => 'false', '[]' => [], '[0]' => [0], 'null' => null, 'new stdClass()' => new stdClass(),
];

foreach ($candidates as $label => $value) {
    printf("%-16s %s\n", $label, $value ? 'true' : 'false');
}
0                false
0.0              false
''               false
'0'              false
'0.0'            true
' '              true
'false'          true
[]               false
[0]              true
null             false
new stdClass()   true

The string "0" being false is a frequent source of bugs: if ($input) rejects a user who typed 0. Compare explicitly ($input !== '') when the distinction matters.

Source: Booleans

int

An int is a signed integer of the platform’s word size: 64 bits on all current platforms, so the range is PHP_INT_MIN (-9223372036854775808) to PHP_INT_MAX (9223372036854775807); PHP_INT_SIZE is 8. Integer literals can be written in decimal, hexadecimal (0x), octal (0o since 8.1, or a leading 0) and binary (0b), with _ separators between digits since PHP 7.4.

<?php

declare(strict_types=1);

$copies   = 1_250_000;     // 7.4: digit separators
$flags    = 0b0000_0101;   // binary: 5
$mode     = 0o755;         // 8.1: explicit octal prefix (493)
$legacy   = 0755;          // the old octal form, still valid
$colour   = 0xFF_A5_00;    // hexadecimal

var_dump($copies, $flags, $mode, $legacy === $mode, $colour);

// Integer overflow does not wrap: the result silently becomes a float.
var_dump(PHP_INT_MAX + 1);              // float(9.223372036854776E+18)
var_dump(is_int(PHP_INT_MAX + 1));      // bool(false)

// Division always yields a float unless both operands divide evenly...
var_dump(7 / 2, 8 / 2);                 // float(3.5) int(4)
// ...so use intdiv() for integer division.
var_dump(intdiv(7, 2));                 // int(3)

Overflowing into a float loses precision without any warning. When integers can exceed 64 bits (money in minor units rarely does; cryptography and identifiers sometimes do), use the GMP or BCMath extensions described in Math, Numbers and Randomness.

Source: Integers

float

A float is an IEEE 754 double-precision number (about 15 significant decimal digits). Literals use a decimal point or an exponent (1.5, 1e3, 1_000.5). PHP_FLOAT_EPSILON, PHP_FLOAT_MIN, PHP_FLOAT_MAX and PHP_FLOAT_DIG describe the format, and NAN and INF are special values.

<?php

declare(strict_types=1);

var_dump(0.1 + 0.2 == 0.3);                              // bool(false)
var_dump(abs((0.1 + 0.2) - 0.3) < PHP_FLOAT_EPSILON);    // bool(true)

var_dump(floor((0.1 + 0.7) * 10));                       // float(7) -- not 8

var_dump(NAN == NAN, is_nan(NAN));                       // bool(false) bool(true)
var_dump(1e308 * 10);                                     // float(INF)

// 8.4: fdiv() and fpow() follow IEEE 754 instead of throwing
var_dump(fdiv(1, 0));                                     // float(INF)

Never use floats for money: store integer minor units (cents) or use BcMath\Number. Never compare floats with ==; compare against a tolerance, or round first.

string

A string is a sequence of bytes, not characters: PHP strings have no encoding of their own. strlen() counts bytes, so strlen('Lem') is 3 but strlen('Łódź') is 7 in UTF-8; the mbstring and intl extensions work with characters (see Unicode, mbstring and intl). Strings can be written with single quotes, double quotes (with escape sequences and interpolation), heredoc and nowdoc; they are mutable byte arrays accessible by offset. All of this is covered in Strings.

Source: Strings

Numeric strings

A numeric string is a string that looks like a number: optional leading whitespace, an optional sign, digits with an optional decimal part and exponent, and optional trailing whitespace (allowed since PHP 8.0). A leading-numeric string starts like a number and continues with other characters ("12 books"); any other string is non-numeric. The distinction drives arithmetic and comparisons:

<?php

declare(strict_types=0);   // these rules apply in coercive mode

var_dump(is_numeric('12'), is_numeric(' 12 '), is_numeric('1e3'), is_numeric('12 books'), is_numeric('0x1A'));
// bool(true) bool(true) bool(true) bool(false) bool(false)

var_dump('12' + 1);          // int(13)    numeric string
var_dump(' 1.5 ' * 2);       // float(3)   whitespace allowed on both sides
var_dump('12 books' + 1);    // int(13)    leading-numeric: works, with a warning
try {
    var_dump('books' + 1);   // non-numeric: TypeError since 8.0
} catch (TypeError $e) {
    echo get_class($e), ': ', $e->getMessage(), "\n";
}
bool(true)
bool(true)
bool(true)
bool(false)
bool(false)
int(13)
float(3)

Warning: A non-numeric value encountered in ... on line 10
int(13)
TypeError: Unsupported operand types: string + int

Before PHP 8.0, arithmetic on a non-numeric string silently produced 0 with a warning, and leading-numeric strings produced only a notice; books written for PHP 7 show that older behaviour.

Source: Numeric strings

array

A PHP array is an ordered map: an ordered collection of key/value pairs, where keys are int or string and values can be anything. The same type serves as list, dictionary, set, stack and queue. array_is_list() (8.1) checks whether the keys are exactly 0, 1, 2, …. Arrays are value types with copy-on-write semantics. Key casting rules, destructuring and the array API are covered in Arrays.

<?php

declare(strict_types=1);

$titles = ['Kindred', 'Solaris'];                      // a list
$byIsbn = ['978-0-00-000001-1' => 'Kindred'];          // a map
var_dump(array_is_list($titles), array_is_list($byIsbn));   // bool(true) bool(false)

Native declarations can only say array; the shape of its keys and values (list<string>, array<string, Book>, array{title: string, year: int}) is expressed in PHPDoc for static analysers.

Source: Arrays and array_is_list

object

An object is an instance of a class, created with new (or by cloning, unserialising, casting an array to object, which yields a stdClass, and so on). The object type accepts any object; a class, interface or enum name accepts instances of that type and its subtypes. Objects are handled by reference-like handles: assigning or passing an object does not copy it. See Classes and Objects.

<?php

declare(strict_types=1);

function describe(object $thing): string
{
    return $thing::class;            // ::class on an object, PHP 8.0
}

echo describe(new ArrayObject()), "\n";              // ArrayObject
echo describe((object) ['title' => 'Kindred']), "\n"; // stdClass

Source: Objects

Enumerations

An enum (8.1) declares a closed set of named values, each case being a singleton object of the enum’s type. Enums are class-types and can be used in declarations: function lend(Format $format) accepts only Format cases. See Enumerations.

Source: Enumerations (type overview)

resource

A resource is a handle to an external resource — an open file or stream, a process, a socket — managed by an extension. It cannot be used in declarations. Most extensions have replaced their resources with proper objects (CurlHandle and GdImage in 8.0, finfo in 8.1, PgSql\Connection in 8.1, …), and the remaining ones (mainly streams from fopen() and proc_open()) are expected to follow. Resources are freed automatically when nothing references them, so fclose() is a matter of releasing a lock or a file descriptor early, not of preventing leaks.

<?php

declare(strict_types=1);

$stream = fopen('php://memory', 'w+');
var_dump(is_resource($stream), get_resource_type($stream));    // bool(true) string(6) "stream"
fclose($stream);
var_dump(is_resource($stream), get_debug_type($stream));       // bool(false) string(17) "resource (closed)"

$curl = curl_init();
var_dump($curl instanceof CurlHandle);                         // bool(true) -- an object since PHP 8.0

callable

callable accepts anything that can be called: a Closure (including arrow functions and first-class callables such as strlen(...)), an object with __invoke(), a function name as a string, [$object, 'method'] or [ClassName::class, 'staticMethod']. It can be used for parameters and return types but not for properties, and it cannot describe a signature. In new code prefer the Closure type and first-class callable syntax; see Closures and Callables.

<?php

declare(strict_types=1);

function apply(callable $fn, string $value): string
{
    return $fn($value);
}

final class Shout { public function __invoke(string $s): string { return strtoupper($s); } }

echo apply('ucfirst', 'kindred'), "\n";                 // Kindred
echo apply(strrev(...), 'kindred'), "\n";               // derdnik
echo apply(new Shout(), 'kindred'), "\n";               // KINDRED
echo apply(fn (string $s): string => "«{$s}»", 'kindred'), "\n";   // «kindred»

iterable and mixed

iterable (7.1) is an alias for Traversable|array: anything foreach can loop over. It is often the best parameter type for functions that only iterate, because it accepts generators as well as arrays.

mixed (8.0) accepts any value, including null; it is the top type. Writing mixed is different from writing nothing: it documents that any value is genuinely allowed. mixed cannot be part of a union (mixed|null is an error) and cannot be nullable.

<?php

declare(strict_types=1);

function countItems(iterable $items): int
{
    $n = 0;
    foreach ($items as $_) { $n++; }
    return $n;
}

function generateIsbns(): Generator { yield '978-0-00-000001-1'; yield '978-0-00-000002-8'; }

var_dump(countItems(['a', 'b', 'c']));          // int(3)
var_dump(countItems(generateIsbns()));          // int(2)

function dump(mixed $value): void { echo get_debug_type($value), "\n"; }
dump(null);                                     // null

Source: Iterables and mixed

void and never

void (7.1) is a return-only type meaning "returns no value": the function may use return; but not return $x;, and its call evaluates to null. never (8.1) is a return-only type meaning "never returns": the function always throws, calls exit() or loops forever. never is the bottom type: it is a subtype of every type, so a child method may narrow any return type to never.

<?php

declare(strict_types=1);

function logLoan(string $isbn): void
{
    if ($isbn === '') {
        return;                     // allowed: no value
    }
    echo "Loaned {$isbn}\n";
}

function fail(string $message): never
{
    throw new RuntimeException($message);
}

function titleOf(array $book): string
{
    return $book['title'] ?? fail('Book without a title');   // fail() satisfies the string return type
}

logLoan('978-0-00-000001-1');
echo titleOf(['title' => 'Kindred']), "\n";
try {
    titleOf([]);
} catch (RuntimeException $e) {
    echo $e->getMessage(), "\n";    // Book without a title
}

Since PHP 8.5, a void function’s result can be explicitly discarded with the (void) cast, which pairs with the #[\NoDiscard] attribute described in Functions.

Source: void and never

self, parent and static

The relative class types refer to classes relative to where they are written: self is the class in which the method is declared, parent its parent class, and static (return type only, 8.0) the class on which the method was actually called, which is what fluent interfaces and named constructors need with inheritance (late static binding, see Cloning, Late Static Binding and Object Lifecycle).

<?php

declare(strict_types=1);

class Query
{
    private array $filters = [];

    public static function create(): static { return new static(); }

    public function where(string $filter): static
    {
        $clone = clone $this;
        $clone->filters[] = $filter;
        return $clone;
    }

    public function filters(): array { return $this->filters; }
}

final class BookQuery extends Query {}

$q = BookQuery::create()->where('year < 1970');
echo $q::class, ' ', implode(', ', $q->filters()), "\n";   // BookQuery year < 1970

true and false

The singleton types false (in unions since 8.0, standalone since 8.2) and true (8.2) accept only that literal. They exist mainly to describe internal functions precisely (strpos() returns int|false) and to let subclasses narrow bool returns. true|false is a compile-time error: write bool.

<?php

declare(strict_types=1);

function firstPosition(string $haystack, string $needle): int|false
{
    return strpos($haystack, $needle);
}

var_dump(firstPosition('Solaris', 'S'));   // int(0) -- 0, not false!
var_dump(firstPosition('Solaris', 'x'));   // bool(false)

Source: Singleton types

Composite types

Union types (8.0)

A union type T|U|V accepts a value of any of its member types. ?T is shorthand for T|null and can only be used with a single type (?int|string is invalid; write int|string|null).

<?php

declare(strict_types=1);

function shelfLabel(int|string $id): string
{
    return is_int($id) ? sprintf('#%05d', $id) : strtoupper($id);
}

echo shelfLabel(42), ' ', shelfLabel('sf-07'), "\n";   // #00042 SF-07

Source: Union types

Intersection types (8.1)

An intersection type T&U accepts only values that are instances of all the listed class-types. It is useful when a function needs an object that implements several interfaces. Only class-types can be intersected (int&string or self&Countable are errors).

<?php

declare(strict_types=1);

function summarise(Countable&Traversable $collection): string
{
    $first = null;
    foreach ($collection as $item) { $first = $item; break; }
    return count($collection) . " items, first: {$first}";
}

echo summarise(new ArrayIterator(['Kindred', 'Solaris'])), "\n";   // 2 items, first: Kindred

DNF types (8.2)

Disjunctive normal form combines both: a union of intersections, where each intersection is parenthesised: (Countable&Traversable)|null or (A&B)|(C&D)|array. Every combination of union and intersection must be written in this normal form.

<?php

declare(strict_types=1);

function maybeCount((Countable&Traversable)|array|null $items): int
{
    return $items === null ? 0 : count($items);
}

var_dump(maybeCount(null), maybeCount([1, 2]), maybeCount(new ArrayObject([1, 2, 3])));   // int(0) int(2) int(3)

Source: Composite types

Redundancy rules

To catch simple mistakes, a composite type with a duplicate or redundant member that can be detected without loading classes is a compile-time error: int|INT, bool|false, object|Book, iterable|array, mixed|null and true|false are all rejected.

Type declarations

Types can be declared on function and method parameters and return values (7.0, nullable 7.1), on properties (7.4) and on class constants (8.3). Every type except resource can be declared; void, never and static are return-only, and callable is not allowed for properties.

<?php

declare(strict_types=1);

final class Catalogue
{
    public const string NAME = 'City Library';     // 8.3: typed class constant

    /** @var array<string, string> isbn => title */
    private array $titles = [];                    // 7.4: typed property

    public ?string $lastAdded = null;              // nullable property, explicitly initialised

    public function add(string $isbn, string $title): static   // parameter and return types
    {
        $this->titles[$isbn] = $title;
        $this->lastAdded = $isbn;
        return $this;
    }

    public function find(string $isbn): ?string
    {
        return $this->titles[$isbn] ?? null;
    }
}

$c = (new Catalogue())->add('978-0-00-000001-1', 'Kindred');
var_dump($c->find('978-0-00-000001-1'), $c->find('nope'), Catalogue::NAME);

Typed properties and the uninitialised state

A typed property without a default value starts uninitialised: reading it before assigning throws an Error, and isset() returns false. This is different from null and is what lets a non-nullable property exist before a constructor runs. Untyped properties default to null.

<?php

declare(strict_types=1);

final class Draft
{
    public string $title;            // no default: uninitialised
    public $notes;                   // untyped: null
}

$d = new Draft();
var_dump(isset($d->title), $d->notes);   // bool(false) NULL
try {
    echo $d->title;
} catch (Error $e) {
    echo $e->getMessage(), "\n";
    // Typed property Draft::$title must not be accessed before initialization
}

Nullable types and the implicitly nullable parameter deprecation

Before nullable types existed, the idiom for an optional object parameter was a null default: function lend(Book $book = null). The = null default implicitly made the type nullable. PHP 8.4 deprecated this implicit nullability, because the type declaration no longer tells the truth; declare it explicitly:

<?php

declare(strict_types=1);

final class Member {}

// Deprecated since 8.4: "Implicitly marking parameter $member as nullable is deprecated"
// function lend(string $isbn, Member $member = null) {}

// Correct: explicit nullable type (works since 7.1)
function lend(string $isbn, ?Member $member = null): string
{
    return $member === null ? "{$isbn} reserved" : "{$isbn} lent";
}

echo lend('978-0-00-000001-1'), "\n";                // 978-0-00-000001-1 reserved
echo lend('978-0-00-000001-1', new Member()), "\n";  // 978-0-00-000001-1 lent

Rector and PHP-CS-Fixer can make this change across a code base automatically (Static Analysis and Refactoring).

Type declarations and references

If a by-reference parameter has a type, the type is checked only when the function is entered, not when it returns, so a function can change the type of the caller’s variable. Typed properties, on the other hand, keep their type even through references: assigning a wrong type through a reference to a typed property throws.

Coercive and strict typing: declare(strict_types=1)

By default, PHP runs scalar type declarations in coercive mode: if a function expecting int receives the string "5" or the float 5.0, the value is converted (when the conversion is lossless and numeric). With declare(strict_types=1); as the very first statement of a file, calls made from that file to functions with scalar parameters, and return statements in that file, accept only the exact type. The single exception is that an int is accepted where a float is expected (widening).

<?php

declare(strict_types=1);

function addCopies(int $copies): int { return $copies + 1; }
function average(float $total, int $count): float { return $total / $count; }

var_dump(average(10, 4));          // float(2.5) -- int to float widening is allowed

try {
    addCopies('5');                // TypeError in strict mode
} catch (TypeError $e) {
    echo $e->getMessage(), "\n";
}
float(2.5)
addCopies(): Argument #1 ($copies) must be of type int, string given, called in ... on line 11

Key points:

  • The mode is decided by the calling file, not the file that declares the function. A library written in strict mode is still called coercively from a file without the declaration.

  • strict_types affects scalar parameter, return and property type checks only. Operators (==, +, .) keep their usual juggling rules.

  • Internal functions follow the caller’s mode too: strlen(42) works in coercive mode but throws in strict mode.

  • Typed property assignments follow the mode of the file performing the assignment.

Almost every modern code base, framework and coding standard (PER Coding Style, the Laravel skeleton’s tooling, Symfony) writes declare(strict_types=1); in every file. This section does the same.

Source: Strict typing

What coercive mode does

In coercive mode, the conversions are: int to float; float to int (a float with a fractional part, or a string such as "5.5", is still truncated, but since 8.1 this emits a deprecation: "Implicit conversion from float 5.5 to int loses precision"); numeric strings to int/float; int, float and Stringable objects to string; and scalars to bool. For union types, PHP tries int, float, string, bool in that order, picking the first that the value can be converted to without loss. null is never coerced for user-defined functions; passing null to a non-nullable parameter of an internal function has been deprecated since 8.1.

<?php

declare(strict_types=0);    // coercive mode (the default when the declare is absent)

function copies(int $n): int { return $n; }

var_dump(copies('12'));     // int(12)
var_dump(copies(12.0));     // int(12)
var_dump(copies(true));     // int(1)
try {
    copies('12 books');     // leading-numeric strings are rejected for parameters
} catch (TypeError $e) {
    echo $e->getMessage(), "\n";
}

Variance

When a child class overrides a method, its parameter types may be wider (contravariance) and its return type narrower (covariance) than the parent’s, and property types must stay the same (invariance) unless hooks make a property read-only or write-only. These rules, which make never and mixed useful as the bottom and top of the lattice, are detailed in Inheritance, Interfaces and Abstract Classes.

Common pitfalls

  • Scalar type aliases are not types. boolean, integer, double in a declaration are treated as class names; PHP warns ("did you mean bool?") and then throws TypeError for every call.

  • ?int versus int = null. The second form is deprecated since 8.4; and ?int without a default still makes the parameter required.

  • Floats and int parameters. In coercive mode 5.0 is accepted silently for int, while 5.5 is truncated to 5 with a deprecation notice (8.1): the outcome depends on the value, not the type. Strict mode rejects both.

  • strict_types must come first. It must be the first statement in the file (only declare(ticks=…) or encoding may accompany it), otherwise it is a compile-time error.

  • Arrays and PHPDoc. array alone says nothing about contents; add @param list<Book> so static analysis can check callers.

Where older books and tutorials are outdated

The books consulted for this section cover PHP 7.0 to 7.3, so they lack everything typed after that:

Feature Since What older material says or omits

Typed properties

7.4

Books declare untyped properties and validate in setters.

Union types, mixed, static return

8.0

Expressed only in PHPDoc.

Saner numeric strings; TypeError for arithmetic on non-numeric strings

8.0

Books show "abc" + 1 === 1 with a warning.

Intersection types, never

8.1

Absent.

DNF types, standalone null/false, true

8.2

Absent.

Typed class constants

8.3

Constants are always untyped in the books.

Implicitly nullable parameters deprecated

8.4

Books recommend Type $x = null as the way to make an object parameter optional.

array and callable reserved as class names

8.5

Absent.