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

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)

Pro Tips

📋 Quick Reference — Types

SyntaxExampleWhat It Does
type $paramfunction f(int $n)Typed parameter
): typefunction f(): stringReturn type
: voidfunction log(): voidReturns nothing
?type?stringNullable (type or null)
a|bint|stringUnion of types
public type $ppublic float $balanceTyped 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!

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

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.

Related lessons