Weak vs. Strict Typing: Pitfalls Beyond declare(strict_types=1)
AI generated
8.4
PHP
Weak vs. Strict Typing in PHP
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.

13 min read Type Juggling strict_types

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.

11. FAQ: Weak vs. Strict Typing

1Does strict_types make the == operator stricter?
No, strict_types has no influence whatsoever on ==, switch comparisons without strict mode, or implicit boolean evaluations.
2Is declare(strict_types=1) alone enough for type safe code?
No, it only covers function arguments and return values. Internal comparisons additionally need consistent === and static analysis.
3Why does '0' == false return true in PHP?
Because PHP converts both operands into a boolean context for this comparison, and the string '0' counts as falsy there, just like an empty string.
4What counts as a numeric string in PHP?
A string that consists entirely of a valid number representation, such as '123' or '1e2'. Such strings are compared as numbers against other numeric values with ==
5How does switch compare its case values?
By default with ==, not ===. That can cause unintended matches in the wrong case for values like 0 or empty strings.
6What does the third parameter of in_array do?
Setting it to true makes in_array compare with === instead of ==, avoiding type juggling caused false matches with mixed types.
7Why is the zero vs empty trap so dangerous?
Because 0, '0', '', null, and false are all treated identically as falsy in an if condition, even though they can represent completely different business states.
8Can PHPStan detect loose comparisons automatically?
With the phpstan-strict-rules extension and a sufficiently high level, PHPStan flags many risky == comparisons between incompatible types.
9Is match a safe alternative to switch?
Yes, match always compares its arms with ===, which structurally removes the type juggling risk that switch carries.
10Should === generally be the default?
Yes, in modern PHP code === is considered the sensible default, while == should be reserved for a few deliberately justified exceptions.