In PHP, what is the uninitialized state of a typed property, and how does it differ from a property holding null?
answer
- typed means no implicit null default
- reading throws an Error, not a warning
- isset() returns false quietly
- nullable type still starts uninitialized
- unset() returns it to uninitialized
basics
~20 sA 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 sSince 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
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
Recall that a typed property without a default has no value until assigned, and that reading it throws an Error.
Explain the difference between uninitialized and null, including nullable types without defaults, isset() behaviour and what unset() does.
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.
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.