skip to content

In PHP, what is the uninitialized state of a typed property, and how does it differ from a property holding null?

level: middleimportance: must knowfreq 55%

answer

  1. typed means no implicit null default
  2. reading throws an Error, not a warning
  3. isset() returns false quietly
  4. nullable type still starts uninitialized
  5. unset() returns it to uninitialized

basics

~20 s

A typed property without a default has no value at all until something assigns it. Reading it throws an Error (must not be accessed before initialization), unlike null, which is a real value. Untyped properties default to null instead.

solid answer

~40 s

Since typed properties arrived in PHP 7.4, a property declared with a type and no default, such as `public string $pnr;`, starts **uninitialized**: it has no value, not even `null`. Reading it throws `Error: Typed property Booking::$pnr must not be accessed before initialization`. That differs from `null`, which is a value you can read, compare and pass around; a nullable `?string $note;` without `= null` is *also* uninitialized. `isset()` and `??` treat an uninitialized property as not set without throwing, and `var_dump()` shows it as `uninitialized(string)`. An **untyped** property keeps the old behaviour and defaults to `null`. You leave the state by assigning a value of the declared type, usually in the constructor; `unset()` on a typed property puts it back into the uninitialized state.

code

php · 25 lines
php
<?php
declare(strict_types=1);

final class Booking
{
    public string $pnr;
    public ?string $note;
    public $legacy;
}

$b = new Booking();
var_dump($b->legacy);          // NULL: untyped defaults to null
var_dump(isset($b->note));     // bool(false): uninitialized, no error
echo $b->pnr ?? 'no PNR', PHP_EOL; // no PNR

try {
    echo $b->pnr;
} catch (Error $e) {
    echo $e->getMessage(), PHP_EOL;
    // Typed property Booking::$pnr must not be accessed before initialization
}

$b->pnr = 'X7K2LM';
unset($b->pnr);                // back to uninitialized, not null
var_dump(isset($b->pnr));      // bool(false)

go deeper

for a junior

Recall that a typed property without a default has no value until assigned, and that reading it throws an Error.

for a middle

Explain the difference between uninitialized and null, including nullable types without defaults, isset() behaviour and what unset() does.

for a senior

Trace uninitialized-property errors back to objects built without their constructor, such as hydrators or reflection, and design classes so every path assigns every property.

for a principal

Set conventions on defaults, nullable types and constructor completeness across a codebase so the uninitialized state signals bugs rather than normal data.

## Two different kinds of "empty" PHP has two ways a property can look empty: - **`null`** is a value. A property holding `null` can be read, compared with `=== null`, passed to functions and returned. - **Uninitialized** means there is no value at all. The property is declared, but reading it is an error. The uninitialized state exists only for **typed properties** (PHP 7.4 and later). An untyped property such as `public $legacy;` still gets an implicit `null` default. ## How a typed property starts | Declaration | Initial state | |---|---| | `public $note;` (untyped) | `null` | | `public string $pnr;` | uninitialized | | `public ?string $note;` | uninitialized, even though the type allows `null` | | `public ?string $note = null;` | `null` | | `public int $seats = 1;` | `1` | The third row surprises people: a nullable type only *permits* `null`; it does not make `null` the default. PHP deliberately gives typed properties no implicit default, so a missing assignment is detected instead of silently producing `null`. ## What each operation does on an uninitialized property 1. **Read** (`$b->pnr`, `echo`, passing it to a function): throws `Error` with the message `Typed property Booking::$pnr must not be accessed before initialization`. 2. **`isset($b->pnr)`** returns `false` without an error, and `$b->pnr ?? 'n/a'` yields the fallback. 3. **`var_dump($b)`** lists the property as `["pnr"]=> uninitialized(string)`. 4. **Write** a value of the declared type: the property becomes initialized. A wrong type throws `TypeError` (coercion rules depend on `strict_types`). 5. **`unset($b->pnr)`** on an initialized typed property returns it to the uninitialized state; it does not set it to `null`. Because the read is an `Error`, not a warning, `catch (Exception $e)` does not catch it. ## Where uninitialized properties come from in practice - A constructor with a path that forgets to assign one property. - Objects created **without running the constructor**: a child class whose constructor never calls the parent's, `ReflectionClass::newInstanceWithoutConstructor()`, or hydration by an ORM or serializer that fills only some properties. - Promoted constructor parameters whose default belongs to the parameter, not the property, when the constructor is skipped. - Deliberate **lazy initialization**: the property is left unset and filled on first access. The resulting `Error` usually appears far from the cause, for example in a template that reads `$booking->pnr` long after a hydrator skipped it. ## How to handle it - **Assign everything in the constructor.** Constructor promotion makes this the default shape. - **Give a real default** when one exists: `public int $seats = 1;`. - **Use `?type` with `= null`** only when "no value" is a legitimate business state, not as a way to silence the error. - **Check with `isset()`** (or reflection's `ReflectionProperty::isInitialized()`) in generic code that may see partially built objects. - **Let static analysis help**: analysers flag typed properties that are not assigned in the constructor. ## Uninitialized, null and undeclared side by side | Situation | Reading it | `isset()` | `property_exists()` | |---|---|---|---| | declared, typed, never assigned | throws `Error` | `false` | `true` | | declared, holding `null` | `null` | `false` | `true` | | not declared at all | warning `Undefined property`, then `null` | `false` | `false` | `property_exists()` is the quickest way to tell a missing declaration from a missing value, and `ReflectionProperty::isInitialized()` separates the first two rows. ## Why PHP chose this design If typed properties defaulted to `null`, a `string` property could hold `null`, which would contradict its type. The uninitialized state keeps the declared type honest: every value you can ever read from `public string $pnr` is a string, and the absence of a value is reported loudly instead of flowing on as `null`. ## Summary Typed properties without a default start uninitialized, which is not `null`. Reading them throws an `Error`, `isset()` reports them as not set, a nullable type does not change this, and `unset()` puts an initialized typed property back into that state.

  • Does `public ?string $note;` start as `null`?
    No. The `?` only allows `null` as a value; it does not supply a default. Without `= null`, the property is uninitialized and reading it throws the same `Error` as a non-nullable one. Write `public ?string $note = null;` when `null` is the intended starting value.
  • How can generic code test whether a typed property is initialized without triggering the Error?
    `isset($obj->prop)` returns `false` for an uninitialized property, but also for one holding `null`. To distinguish the two, use `ReflectionProperty::isInitialized($obj)`, which returns `true` for an initialized property even when its value is `null`.

saying these in an interview costs you the question

  • A typed property without a default starts as null.
  • Declaring the type as ?string makes null the default value.
  • Reading an uninitialized property emits a warning and returns null.
  • isset() on an uninitialized typed property throws an Error.
  • unset() on a typed property sets it to null.