In PHP, what does a nullable type such as ?Customer mean, and is it the same as Customer|null or an optional parameter?
answer
- question mark before one type
- ?T and T|null are identical
- nullable is not optional
- return null; explicitly, not return;
- T $x = null deprecated in 8.4
basics
~20 s?Customer accepts a Customer object or null, and is exactly the same type as Customer|null. Nullable does not mean optional: the argument must still be passed unless the parameter also has a default, written ?Customer $c = null.
solid answer
~40 sA leading `?` (PHP 7.1) marks a **single** type as nullable: `?Customer` accepts a `Customer` or `null`, and the manual states that `?T` and `T|null` are identical. The `?` cannot prefix a union — `?int|string` is a syntax error, so you write `int|string|null`. **Nullable is not optional**: `function assign(?Customer $c)` still requires an argument, and calling it with none throws `ArgumentCountError`; `?Customer $c = null` is both. On a return type, `?Customer` requires an explicit `return null;` — a bare `return;` does not compile, and falling off the end throws `TypeError`. The old shortcut `Customer $c = null`, which made the type nullable implicitly, is deprecated since PHP 8.4.
code
php · 20 lines<?php
declare(strict_types=1);
final class Customer
{
public function __construct(public string $name) {}
}
function greet(?Customer $customer): string // nullable, but required
{
return 'Hello, ' . ($customer?->name ?? 'guest');
}
echo greet(null), PHP_EOL; // Hello, guest
try {
echo greet();
} catch (ArgumentCountError $e) {
echo 'missing argument', PHP_EOL; // nullable is not optional
}go deeper
Recall that ?Customer accepts a Customer or null, that it equals Customer|null, and that you still must pass the argument unless there is a default.
Explain why ? works on one type only, the explicit return null rule for nullable returns, and why PHP 8.4 deprecated the implicit T $x = null form.
Decide where null is a legitimate result and where absence should throw, since every nullable return pushes a check onto every caller.
Set a codebase convention for representing absence — nullable returns, exceptions or dedicated result objects — and keep it consistent across modules.
## What the question mark does In a PHP type declaration, a leading `?` means **"this type, or null"**. It was added in PHP 7.1, before PHP had general union types: ```php <?php declare(strict_types=1); function describe(?Customer $customer): string { return $customer === null ? 'guest' : $customer->name; } ``` `describe(new Customer(...))` and `describe(null)` both work; `describe('bob')` throws `TypeError`. ## ?T and T|null are the same type PHP 8.0 added union types, and with them a second spelling. The manual says it directly: `?T` and `T|null` are identical. Reflection, error messages and the engine treat them as one type. Which spelling to use is style: - `?Customer` is shorter and the common choice for one type plus null. - `int|string|null` is **required** when more than one non-null type is involved, because `?` applies only to a single type. `?int|string` does not parse. - For an intersection plus null, PHP 8.2+ needs the DNF form `(A&B)|null`; `?` cannot prefix an intersection either. Two spellings are rejected outright: `?mixed` (mixed already includes null) and `?null`. ## Nullable is not optional This is the part candidates most often blur. **Nullable** is about which values are accepted; **optional** is about whether the argument may be omitted. They are independent: | Declaration | `f(null)` | `f()` | |---|---|---| | `function f(?Customer $c)` | accepted | `ArgumentCountError` | | `function f(?Customer $c = null)` | accepted | accepted, `$c` is `null` | | `function f(Customer $c)` | `TypeError` | `ArgumentCountError` | | `function f(Customer $c = null)` | accepted (declaration deprecated since 8.4) | accepted, `$c` is `null` | The last row is the old implicit form: a `null` default silently widened the type to nullable. PHP 8.4 deprecated it because the declared type no longer says what the parameter accepts. The fix is to write the `?` explicitly. ## Nullable return types On a return type, `?Customer` means the function returns a `Customer` or `null` — but it must **say** which: 1. `return null;` is fine. 2. A bare `return;` is a compile error: `A function with return type must return a value (did you mean "return null;" instead of "return;"?)`. 3. Reaching the end of the body without any `return` throws `TypeError` at runtime (`Return value must be of type ?Customer, none returned`). PHP does not treat "no return" as "return null" for a declared return type. The rule exists to separate a deliberate "not found" from a forgotten return. ## When to use a nullable type - **Lookups that can miss**: `findCustomer(int $id): ?Customer` returns `null` for "no such customer". The caller must handle it, and the nullsafe operator `?->` or an explicit check makes that visible. - **Truly optional inputs**: `?string $note = null` for an argument that may be absent. - **Avoid it where absence is an error.** If a missing customer is a bug, return `Customer` and throw; a nullable return pushes a check onto every caller. ## Handling a nullable value Once a value may be `null`, the code that receives it has to deal with that before using it as an object: - an explicit check: `if ($customer === null) { ... }`; - the **nullsafe operator** (PHP 8.0): `$customer?->name` evaluates to `null` instead of throwing when `$customer` is `null`; - the **null coalescing operator**: `$customer?->name ?? 'guest'` supplies a fallback. Calling a method directly on a `null` value, `$customer->name()`, throws an `Error`. That is the practical cost of a nullable type: every consumer carries a branch, which is why it should mark genuine absence rather than serve as a convenient default. ## Common mistakes 1. Treating `?Customer $c` as optional and then calling the function without an argument. 2. Writing `?int|string` and getting a syntax error, instead of `int|string|null`. 3. Returning with a bare `return;` from a `?Customer` function, which does not compile. 4. Keeping `Customer $c = null` in new code, which raises a deprecation on PHP 8.4 and later. ## Quick reference - `?T` = `T|null`, since PHP 7.1; the union spelling since 8.0. - `?` works on one type only. - Nullable ≠ optional; add `= null` for optional. - `T $x = null` without `?` is deprecated since PHP 8.4. - A `?T` return needs an explicit `return null;`.
- In PHP, how do you declare a parameter that accepts an int, a string or null?Write the union with `null` as a member: `int|string|null $key`. The `?` shorthand applies to one type only, so `?int|string` is a syntax error. If the argument may also be omitted, add a default: `int|string|null $key = null`.
- In PHP, what happens when a function declared : ?Customer reaches the end of its body without a return statement?It throws `TypeError` saying the return value must be of type `?Customer` and none was returned. A declared return type is never satisfied by an implicit return, even when `null` is allowed. Write `return null;` for the not-found path; a bare `return;` would not compile.
saying these in an interview costs you the question
- Says ?Customer makes the parameter optional, so it can be left out.
- Thinks ?Customer and Customer|null behave differently at runtime.
- Writes ?int|string for an int, a string or null.
- Uses a bare return; to return null from a function declared ?Customer.
- Recommends Customer $c = null as the normal way to declare a nullable parameter.