What declare(strict_types=1) Actually Does
The declare(strict_types=1) directive must be the first executable line in a file, right after the opening <?php tag. It does not enable typing as such — parameter and return types can be declared without it too. It changes only one thing: how PHP behaves when a passed value does not match the declared type.
Without strict_types, PHP tries to coerce the value automatically: the string "42" becomes the int 42, the number 1 becomes the bool true. With strict_types=1, any type mismatch, except the safe widening of int to float, throws a TypeError immediately at the point the function is called, not somewhere inside it after a few lines of logic.
Weak Typing: an Example of Implicit Conversion
The function below declares an int parameter, but without strict_types PHP will accept both a string and a floating-point number, silently cutting off the fractional part.
function applyDiscount(int $percent): float {
return 100 - $percent;
}
echo applyDiscount("15abc"); // works as applyDiscount(15), no error
The string "15abc" is not a number, but PHP in weak mode cuts off the part that looks like a number and runs the function. The bug shows up not here but wherever the result turns out unexpectedly wrong — you end up hunting for it through the logs, as covered in the article on reading PHP logs.
With strict_types=1: the Same Error Is Caught Immediately
The same code with the directive added at the top of the file behaves differently — instead of a silent conversion, it fails explicitly.
declare(strict_types=1);
function applyDiscount(int $percent): float {
return 100 - $percent;
}
echo applyDiscount("15abc");
// TypeError: applyDiscount(): Argument #1 ($percent) must be of type int, string given
The error appears right at the call site, with an exact line number, instead of after several steps of business logic. This matters especially for code written for PHP 8.3 and 8.4 — the new features of these versions are covered in the article on PHP 8.3 and 8.4, and they assume strict typing by default.
Property, Parameter, and Return Types: Examples
| Construct | Example | What strict_types Checks |
|---|---|---|
| Parameter type | function f(int $x) | argument type at the call site |
| Return type | function f(): string | value type after return |
| Class property type | private float $price | type when assigning to the field |
| Nullable type | function f(?int $x) | allows int or null, not a string |
Nullable types and union types like int|string work just as strictly with strict_types: any value outside the declared list throws a TypeError.
Where Strict Typing Breaks Existing Code
- Data from $_GET and $_POST always arrives as strings — cast it explicitly with (int) before passing it into a typed function.
- Values from PDO are also strings by default, even for numeric columns — without an explicit cast, strict_types throws a TypeError.
- Old libraries without declared parameter types keep working as before; strict_types does not affect them.
- JSON after json_decode returns numbers as int or float depending on the content — check the type before passing it further.
- Tests written assuming automatic type coercion start failing and need explicit casts in the mocks.
Summary: Checklist for Adopting strict_types
- declare(strict_types=1) is the first line after <?php in every file of the project.
- All input data from $_GET, $_POST, and PDO is cast to the right type explicitly, instead of relying on automatic coercion.
- Parameters and return values of public methods have declared types, including nullable and union types.
- New TypeError occurrences are tracked through error_log, not through display_errors in production.
- The change is rolled out gradually, module by module, with tests run after every file.