What declare(strict_types=1) really secures and where type juggling still bites
Many developers put declare(strict_types=1) at the top of every file and believe this fully protects their code against PHP's notorious type juggling surprises. In reality strict_types only covers part of the problem, namely function arguments and return values. Internal comparisons with == remain completely untouched and weakly typed. This article pins down exactly what strict_types secures and describes concrete pitfalls experienced PHP developers still run into regularly.
Table of Contents
- 1. What declare(strict_types=1) actually covers
- 2. What strict_types does NOT cover: internal comparisons
- 3. Type juggling with == in detail
- 4. The PHP 8 change to number to string comparisons
- 5. The numeric string trap
- 6. The zero vs empty trap in conditions
- 7. Pitfalls in switch and in_array without strict mode
- 8. How PHPStan and Psalm close the gap
- 9. Practical rules for everyday use
- 10. Summary
- 11. FAQ
1. What declare(strict_types=1) actually covers
declare(strict_types=1) only changes behavior when passing arguments to functions and methods, and for their return values. Without strict_types, PHP would try to automatically convert a passed string like '42' into an expected int parameter. With strict_types enabled, that call fails with a TypeError instead, except for the one allowed exception: int values may still be passed losslessly to a float parameter.
Importantly, this directive works per file and applies to the calling context, not to the function's own definition. If a file without strict_types calls a strictly typed function defined in another file, the loose typing of the calling file still applies to that call. So anyone writing a library has no guarantee that strict_types is even active on the consumer side, the behavior depends solely on the file where the call is written.
2. What strict_types does NOT cover: internal comparisons
The most common misconception is assuming strict_types also tightens the == comparison operator. That is wrong: strict_types has no influence whatsoever on ==, in, switch cases without a strict mode, or implicit boolean evaluations in if conditions. The expression '0' == false still returns true, completely independent of whether strict_types is set in the file or not.
Concretely this means strict_types solves exactly one class of bugs, namely mistyped function calls, but leaves the second big class of errors, unexpected results from loose comparisons, entirely untouched. Experienced developers who rely on strict_types alone are lulled into a false sense of safety here and overlook exactly the places where == with mixed types shows up in the code.
3. Type juggling with == in detail
The loose comparison operator == converts one of the two operands when their types differ, before comparing them. Which value gets converted and in which direction follows a set of rules PHP documents in its official comparison table, rules that regularly surprise even experienced developers. A numeric string is converted to a number when compared against an int, while a bool value forces the other operand into a boolean evaluation.
It gets particularly dangerous with comparisons inside arrays, such as in_array or array_search without the third strict parameter. Searching with in_array('admin', $roles) where $roles happens to contain the value 0 still returns false for '0' == 'admin', but 0 == 'admin' actually returned true in PHP versions before 8.0, because the string got converted to a number. Since PHP 8 this specific behavior has been tightened, but the underlying risk with == remains for other type combinations.
<?php
declare(strict_types=1);
// strict_types offers no protection at all here, == stays loose
var_dump('0' == false); // true
var_dump('0.0' == '0'); // true
var_dump(' 1' == '1'); // true, leading whitespace is ignored
var_dump('1e2' == '100'); // true, both are numerically equal
// Safe alternative: enforce identical type and value
var_dump('0' === false); // false
var_dump('1e2' === '100'); // false
4. The PHP 8 change to number to string comparisons
Before PHP 8.0, comparing an int against a non numeric string first converted the string into a number, which led to the infamous result of 0 == 'foo' being true, because 'foo' converted to 0. Since PHP 8.0 a far more intuitive rule applies: if the string is not numeric, the number is instead converted into a string and both values are compared as strings, which makes 0 == 'foo' correctly return false today.
This change significantly defuses one of PHP's most notorious historical pitfalls, but it does not fully resolve the underlying problem of loose comparisons. For two numeric strings, or for an int against a numeric string, numeric conversion still applies, and that is exactly where the surprises shown in the previous section, involving leading zeros or scientific notation, continue unchanged.
5. The numeric string trap
PHP internally distinguishes between numeric and non numeric strings, and this distinction influences how == behaves. A string like '123' counts as fully numeric, a string like '123abc' does not. When two strings that are both numeric are compared, PHP converts them to numbers for the comparison, independent of == or declared types. That leads to surprises like '010' == '10', even though both strings clearly differ as character sequences.
This becomes especially relevant with input data from forms or CSV files, which is inherently stored as strings. A postal code like '00500' gets treated as numerically equal to '500' when compared, even though these could represent two functionally different values. Anyone comparing such data should work explicitly with === and consistent string normalization, instead of relying on automatic numeric interpretation.
6. The zero vs empty trap in conditions
Another classic trap involves implicit boolean evaluation in if conditions, which is likewise untouched by strict_types. The values 0, 0.0, '0', '', null, false, and an empty array all count as falsy in PHP and are treated identically in an if condition. Code like if (!$quantity) for an order quantity handles an actual quantity of 0 exactly the same as a missing value of null, even though both represent very different situations functionally.
The reliable alternative is an explicit check with === null or a strict type check with is_int combined with an explicit range check. Especially for quantities, prices, or IDs where 0 can be a valid, meaningful value, an implicit boolean check regularly causes bugs that only surface in production with real edge case data.
<?php
declare(strict_types=1);
function applyDiscount(?int $quantity): string
{
// Error prone: 0 and null are treated identically
if (!$quantity) {
return 'No quantity provided';
}
// Correct: distinguishes a missing value from an actual zero
if ($quantity === null) {
return 'No quantity provided';
}
if ($quantity === 0) {
return 'Quantity is explicitly zero';
}
return sprintf('Discount for %d units', $quantity);
}
7. Pitfalls in switch and in_array without strict mode
switch statements also compare their case values with == by default, not ===. A switch over a value that happens to be 0 or an empty string can unintentionally land in a case meant for a completely different value, such as case false, if one exists. Since PHP checks cases from top to bottom and stops at the first loose match, the result is often hard to predict.
The same principle applies to in_array and array_search: without the third true parameter, every comparison runs through ==. With arrays of mixed types, for example IDs that arrive partly as int and partly as string from different data sources, this leads to inconsistent results. The simple rule is: in_array and array_search should practically always be called with the third parameter set to true in modern code.
8. How PHPStan and Psalm close the gap
Because strict_types does not secure the == comparison operator, static analysis tools take over that job. PHPStan, at a sufficiently high level combined with the phpstan-strict-rules extension, flags suspicious loose comparisons between incompatible types, such as comparing an int against a string that is clearly not numeric. Psalm offers similar coverage for implicit boolean evaluations through its RiskyTruthyFalsyComparison level.
In practice it is worth configuring both tools as strictly as possible in the CI pipeline and checking explicitly for loose comparisons, instead of relying on code review attention alone. Combining strict_types at function boundaries with rigorous static analysis for internal comparisons reliably closes the two biggest gaps in PHP's type system.
9. Practical rules for everyday use
A simple rule of thumb helps day to day: === is the default, == is the deliberate, documented exception. Exceptions should be limited to a few clearly justified cases, such as an intentional comparison between int and float in a numeric calculation, where automatic conversion is actually desired. Every other use of == deserves a critical question during code review.
In summary: declare(strict_types=1) is a necessary but not sufficient measure for type safe PHP code. Only the combination of strict_types at function boundaries, consistent === for internal comparisons, and a strictly configured static analysis covers both halves of PHP's type system and reliably prevents the typical surprises of type juggling.
| Situation | Does strict_types apply? | Risk without care | Recommendation |
|---|---|---|---|
| Function argument | Yes | TypeError instead of conversion | Keep strict_types active |
| Return value | Yes | TypeError instead of conversion | Keep strict_types active |
| == comparison | No | Type juggling, false equality | Use === |
| switch/case | No | Unexpected case match | Use match instead of switch |
| in_array/array_search | No | Wrong matches with mixed types | Set the third parameter to true |
| if condition with scalar | No | Zero vs empty confusion | Explicitly check === null or is_int |
Mironsoft
PHP modernization, code quality, and legacy refactoring
Grown PHP code nobody wants to touch anymore?
We modernize PHP codebases to current language standards, introduce static analysis and coding standards, and refactor legacy code step by step without endangering live operations.
Legacy Refactoring
Modernize grown PHP code in a structured, low-risk way.
Establishing Code Quality
Anchor PHPStan, coding standards, and CI checks sustainably in the team.
Version Upgrades
Plan and execute PHP major version upgrades safely, without downtime.
10. Summary
Weak vs. Strict Typing
Scope
strict_types only applies to function arguments and return values, not to ==
Type juggling
== converts operands by fixed rules, numeric strings are a main risk.
Zero vs empty
0, '0', '', null, and false all count as falsy in if conditions.
Safety net
PHPStan with phpstan-strict-rules and Psalm catch loose comparisons statically.