Type Declarations & Typed Properties
Reviewed & published by Brayan K
PHP is dynamically typed, but you don't have to fly blind. Type declarations let you state what each parameter, return value, and property should be — and PHP enforces it. Turn on strict_types and a whole class of bugs becomes an immediate, located error.
Part of the free PHP course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- Declare parameter and return types on functions
- Understand coercive vs strict mode with strict_types
- Use the void and never return types
- Write nullable (?int) and union (int|string) types
- Add typed properties to classes — and handle uninitialised ones
- Catch type mistakes early as a TypeError
1️⃣ Typing Parameters & Returns
Put a type before each parameter, and after the closing parenthesis put : type for the return. PHP enforces these on every call. The scalar types are int, float, string, bool; plus array, object, class names, and the return-only void. By default PHP runs in coercive mode and will convert a compatible value (note the string "21" below becoming an int).
<?php
// Type declarations let you state the type of parameters and the return value.
// PHP then enforces them — passing the wrong type is an error, not a silent bug.
function addPrices(float $a, float $b): float {
return $a + $b;
}
echo addPrices(9.99, 5.00) . "\n"; // 14.99
// The ': void' return type means "returns nothing".
function shout(string $msg): void {
echo strtoupper($msg) . "!\n";
}
shout('hello'); // HELLO!
// Without strict_types, PHP COERCES compatible values:
function double(int $n): int { return $n * 2; }
echo double('21') . "\n"; // 42 — the string "21" is coerced to int 21
?>2️⃣ Strict Mode
Add declare(strict_types=1); as the very first statement of a file to turn off coercion: types must now match exactly, and a mismatch throws a TypeError. This is the recommended default for serious code — it catches wrong types right at the boundary instead of letting a silently-converted value cause a subtle bug deeper in.
<?php
declare(strict_types=1); // MUST be the very first statement in the file.
// In strict mode PHP no longer coerces — types must match EXACTLY.
function double(int $n): int {
return $n * 2;
}
echo double(21) . "\n"; // 42 — an int, fine
try {
echo double('21'); // string given, int expected -> TypeError
} catch (\TypeError $e) {
echo "Rejected: a string is not an int in strict mode\n";
}
?>3️⃣ Nullable & Union Types
Real values are sometimes "or nothing". A leading ? makes a type nullable: ?string means a string or null (shorthand for string|null). For multiple real types, PHP 8.0's union syntax lists them with a pipe: int|string.
<?php
declare(strict_types=1);
// A leading ? makes a type NULLABLE: the value OR null.
function findName(?int $id): ?string {
return $id === null ? null : "User #$id";
}
echo findName(7) . "\n"; // User #7
var_dump(findName(null)); // NULL
// UNION types (PHP 8.0) allow several types: int|string, etc.
function idLabel(int|string $id): string {
return "ID is " . $id;
}
echo idLabel(42) . "\n"; // ID is 42
echo idLabel('abc') . "\n"; // ID is abc
?>4️⃣ Typed Properties
Since PHP 7.4 you can type class properties directly. There's one gotcha worth burning into memory: a typed property with no default isn't null — it's uninitialised, and reading it before you assign throws an error. Give it a default, set it in the constructor, or make it nullable if "not set yet" is valid.
<?php
declare(strict_types=1);
// TYPED PROPERTIES (PHP 7.4) put a type on each class property.
// An UNINITIALISED typed property has NO null default — reading it errors.
class Account {
public string $owner; // typed, but not yet initialised
public float $balance = 0.0; // typed with a default
public ?string $note = null; // nullable, defaults to null
public function __construct(string $owner) {
$this->owner = $owner; // must set it before reading
}
}
$acc = new Account('Ada');
echo $acc->owner . ' has $' . $acc->balance . "\n"; // Ada has $0
try {
$bad = new Account('Sam');
$bad->balance = 'lots'; // wrong type -> TypeError
} catch (\TypeError $e) {
echo "Balance must be a float\n";
}
?>Now you try — fill in each ___ using the 👉 hint, then run it and check against the Output panel.
<?php
declare(strict_types=1);
// 🎯 YOUR TURN — fill in each blank marked ___ , then run it.
// 1) Type both parameters as int and the return as int:
function multiply(___ $a, int $b): ___ { // 👉 the whole-number type, twice
return $a * $b;
}
echo multiply(6, 7) . "\n"; // should print: 42
// 2) Make the parameter NULLABLE so it accepts a string OR null:
function greet(___string $name): string { // 👉 the symbol that means "or null"
return $name ?? 'guest';
}
echo greet(null) . "\n"; // should print: guest
// ✅ Expected output:
// 42
// guest
?>🧩 Reorder Challenge
These lines make a strictly-typed file that prints 10 — but they're scrambled. Put them in the order PHP requires.
C → D → B → E → A
declare(strict_types=1) must be first (C) — it's a fatal error anywhere else. Then define the function: signature (D), body (B), close (E), and finally call it (A), which prints 10.
🧠 Quick Recall — Predict the Output
1. Coercive mode (no strict_types). What prints?
10 — in coercive mode PHP converts the numeric string '9' to the int 9, then adds 1.
2. With declare(strict_types=1) at the top, what happens?
A TypeError is thrown — strict mode refuses to coerce the string '9' into an int.
3. What does reading $u->name do here?
It throws an Error: "Typed property U::$name must not be accessed before initialization." It has no value yet — not even null.
Common Errors (and the fix)
- "Typed property must not be accessed before initialization" — you read a typed property before assigning it. Set it in the constructor, give a default, or make it nullable.
- "strict_types declaration must be the very first statement" — move declare(strict_types=1); to the top, above everything (even comments are fine, but no code before it).
- "TypeError: ... must be of type int, string given" — a real type mismatch. Pass the right type, or cast deliberately with (int) if conversion is intended.
- "A void function must not return a value" — a : void function used return $x;. Use a bare return; or change the return type.
Pro Tips
- 💡 Put declare(strict_types=1); at the top of every file. It's the single biggest reliability upgrade for typed PHP.
- 💡 Prefer ?Type over Type|null for the common "this or nothing" case — it reads cleaner.
- 💡 Type your properties. They double as documentation and let your editor and tools catch mistakes before runtime.
📋 Quick Reference — Types
| Syntax | Example | What It Does |
|---|---|---|
| type $param | function f(int $n) | Typed parameter |
| ): type | function f(): string | Return type |
| : void | function log(): void | Returns nothing |
| ?type | ?string | Nullable (type or null) |
| a|b | int|string | Union of types |
| public type $p | public float $balance | Typed property |
| declare(strict_types=1) | declare(strict_types=1); | Turn off coercion (file-wide) |
Mini-Challenge: Typed Temperature Class
No code is filled in this time — just a brief and an outline. Write it yourself, run it on onecompiler.com/php or your own machine, then check your result against the expected output in the comments.
<?php
// 🎯 MINI-CHALLENGE: a strictly-typed Temperature class.
// No code is filled in — work from the steps below, then run it.
//
// 1. Put declare(strict_types=1); at the top.
// 2. Make a class Temperature with a typed property: private float $celsius.
// Use constructor promotion: __construct(private float $celsius) {}
// 3. Add a method toFahrenheit(): float that returns
// $this->celsius * 9 / 5 + 32
// 4. Create new Temperature(25.0) and echo:
// 25C = 77F (call toFahrenheit() for the second number)
//
// ✅ Expected output:
// 25C = 77F
// your code here
?>🎉 Lesson Complete!
- ✅ Declare types on parameters, returns, and properties — PHP enforces them
- ✅ declare(strict_types=1); (first line!) turns off silent coercion
- ✅ ?type is nullable; a|b is a union of types
- ✅ A typed property with no default is uninitialised, not null — set it before reading
- ✅ Mismatches throw a TypeError you can catch at the boundary
Practice quiz
In coercive mode (no strict_types), what does double('21') print for function double(int $n): int { return $n * 2; }?
- A TypeError
- 2121
- 42
- 0
Answer: 42. Coercive mode converts the numeric string '21' to int 21, then doubles it to 42.
With declare(strict_types=1), what does calling double('21') do?
- Throws a TypeError
- Returns 42
- Returns '2121'
- Returns null
Answer: Throws a TypeError. Strict mode refuses to coerce a string into an int, so a TypeError is thrown.
Where must declare(strict_types=1); appear?
- Anywhere in the file
- At the end of the file
- Inside every function
- As the very first statement of the file
Answer: As the very first statement of the file. It must be the very first statement; placing it anywhere else is a fatal error.
What does ?string mean as a type?
- An optional argument
- A string or null
- A string only
- A union of string and int
Answer: A string or null. A leading ? makes a type nullable: ?string is shorthand for string|null.
Which type accepts both an int and a string argument?
- int|string
- int&string
- ?int
- int,string
Answer: int|string. Union types (PHP 8.0) list multiple types with a pipe: int|string.
What happens when you read a typed property that has no default before assigning it?
- It returns null
- It returns 0
- An Error: it must not be accessed before initialization
- It returns an empty string
Answer: An Error: it must not be accessed before initialization. A typed property with no default is uninitialised — not null — and reading it before assignment throws an Error.
Even in strict mode, which conversion does PHP still allow for typed arguments?
- Passing a string where an int is expected
- Passing an int where a float is expected (widening)
- Passing an array where a string is expected
- Passing a bool where a string is expected
Answer: Passing an int where a float is expected (widening). The one sanctioned exception is widening an int to a float, because it loses nothing.
What does the void return type mean?
- The function returns null
- The function returns an empty array
- The function never finishes
- The function returns nothing
Answer: The function returns nothing. : void declares that a function returns no value; using return $x; in it is an error.
Which is the correct way to give a nullable typed property a starting value?
- public string $note = null;
- public ?string $note = null;
- public string $note;
- public ?string $note;
Answer: public ?string $note = null;. A nullable property can default to null: public ?string $note = null; — a non-nullable string cannot.
What is the recommended default for serious typed PHP code?
- Avoid type declarations entirely
- Use only union types
- Enable declare(strict_types=1) in every file
- Rely on coercion everywhere
Answer: Enable declare(strict_types=1) in every file. Most teams enable strict types everywhere because catching a TypeError at the boundary beats chasing wrong values.
Continue this course
- Previous: Traits
- Next: Dates & Times (DateTime) — Parse, format, compare, and do arithmetic with DateTime and DateInterval
- Quick reference: PHP cheat sheet
Frequently asked questions
What does declare(strict_types=1) actually change?
Without it, PHP runs in coercive mode: when a value's type doesn't match a declaration but is compatible, PHP quietly converts it — the string '21' becomes the int 21, for example. With declare(strict_types=1) at the very top of the file, that automatic coercion is switched off for calls made from that file, and a mismatched type throws a TypeError instead. The one exception PHP still allows is widening an int to a float. Most teams enable strict types everywhere because catching a TypeError at the boundary beats chasing a wrong value deep in the code.
What's the difference between a nullable type (?int) and a union type (int|null)?
They mean the same thing: ?int is shorthand for int|null, allowing either an int or null. The ? prefix only works for a single type, so it's the concise choice when null is the only extra option. Union syntax (added in PHP 8.0) is more general — you can list several non-null types like int|string or even int|string|null. Use ?Type for the common 'this or null' case, and the pipe syntax when you genuinely accept multiple real types.
Why does reading a typed property throw an error before I set it?
A typed property without a default value starts in an 'uninitialised' state — it has no value at all, not even null (unless its type is nullable). Reading it before assignment throws 'Error: Typed property must not be accessed before initialization'. This is deliberate: it catches the bug of forgetting to set a required field. The fixes are to assign it in the constructor, give it a default (public float $x = 0.0;), or make it nullable (public ?string $x = null;) if 'no value yet' is a legitimate state.
Should I add types to everything, or is it overkill?
For anything beyond a throwaway script, yes — type as much as you reasonably can: parameters, return types, and properties. Types act as machine-checked documentation, let your editor autocomplete and catch mistakes, and turn a whole class of runtime surprises into immediate, located errors. Combined with declare(strict_types=1), they make refactoring far safer. The main place you'll relax is when a value genuinely can be several shapes, where a union or a more general type like iterable or mixed is the honest declaration.