from silent type conversion to an immediate TypeError
strict_types turns off PHP's automatic, silent type conversion for the current file: a string passed to an int parameter, a float passed to an int parameter, or a wrong return type all trigger an immediate TypeError instead of being quietly converted to another type, which surfaces bugs years earlier, right at the call site instead of somewhere deeper in the code.
Table of Contents
- 1. Coercive typing vs. strict typing in PHP
- 2. declare(strict_types=1) in detail: syntax and scope
- 3. What concretely changes for function arguments
- 4. Return values and strict_types
- 5. Why int to float widening remains allowed anyway
- 6. strict_types and internal PHP functions
- 7. Why the effect is per-file, not project-wide
- 8. Catching errors early: TypeError instead of silent conversion
- 9. Migrating existing codebases to strict_types incrementally
- 10. Summary
- 11. FAQ
1. Coercive typing vs. strict typing in PHP
PHP has always had two fundamentally different modes for handling type declarations: coercive typing, the default mode, automatically converts a passed value into the declared type whenever a sensible conversion seems possible. A string "42" silently becomes 42 when passed to an int parameter, a float 3.9 gets truncated to 3. strict_types turns off exactly this behavior for the current file.
Without strict_types, PHP thereby conceals potential bugs at the boundary between functions: a typo that passes a string instead of a number does not stand out as long as the string looks numeric. Only once a genuinely non-numeric string is passed, for instance from a broken form field, does the error become visible, often at a completely different spot in the code than where the actual mistake originated.
strict_types fundamentally changes this behavior: once this declaration is active, a function only accepts exactly the declared type, with a single exception for widening int to float. Every other type mismatch immediately throws a TypeError exception, right at the call site, not somewhere deeper in the processing chain.
2. declare(strict_types=1) in detail: syntax and scope
The declaration declare(strict_types=1); must be the very first statement of the file, directly after the opening <?php tag, before any other statement including namespace declarations and use imports. Attempting to place it later in the file results in a compile error, since PHP must know the mode before any other code in the file is evaluated.
Important to understand: strict_types is a per-file setting, not a global switch for the entire project and not a class property. Every .php file decides independently whether it activates this mode. That allows a gradual introduction in existing projects, but it also means that different files in the same project can be differently strict, which is examined more closely in section 7.
<?php
// Must be the very first statement in the file, before namespace and use
declare(strict_types=1);
namespace App\Billing;
use App\Billing\Exception\InvalidAmountException;
final class Invoice
{
public function __construct(
private readonly int $amountInCents,
) {
}
}
3. What concretely changes for function arguments
The most visible effect of strict_types shows up with function arguments. Without strict_types, a function with parameter int $amount also accepts the string "100", the string gets automatically converted to 100. With strict_types active, that exact call throws a TypeError, because a string is not an integer, regardless of whether it looks numeric.
This strictness applies equally to all scalar types: string, int, float, and bool each accept only the exact matching type in strict mode. A bool parameter therefore accepts neither 1 nor "true" nor 0, only genuine boolean values. This consequence is deliberate: strict_types is meant to eliminate implicit assumptions about compatible types, not merely reduce them.
<?php
declare(strict_types=1);
function applyDiscount(int $amountInCents, float $percentage): int
{
return (int) round($amountInCents * (1 - $percentage / 100));
}
// Works: exact types match the declaration
$result = applyDiscount(1999, 10.0);
// Throws TypeError: strict_types rejects a string for an int parameter
// $result = applyDiscount('1999', 10.0);
// Works: int widens to float automatically, the one allowed exception
$result = applyDiscount(1999, 10);
4. Return values and strict_types
strict_types is not limited to parameters, it applies equally to declared return types. A function with : int as its return type must actually return an integer in strict mode. If the function body instead returns a numeric string, PHP throws a TypeError, even if the function itself has no explicit parameter type checks.
This symmetry between parameters and return values matters, because it ensures a function consistently honors its own declared contract, not only when accepting values but also when handing them out. Anyone writing a function with a strict return type can rely, at the call site, on the returned value matching the declared type exactly, without having to validate the return value defensively themselves.
<?php
declare(strict_types=1);
final class PriceCalculator
{
// Declared return type ": int" is enforced strictly on every path
public function finalPriceInCents(int $basePrice, float $taxRate): int
{
$withTax = $basePrice * (1 + $taxRate);
// Explicit cast required: without it, a float would violate
// the declared ": int" return type under strict_types
return (int) round($withTax);
}
}
$calculator = new PriceCalculator();
echo $calculator->finalPriceInCents(1999, 0.19);
5. Why int to float widening remains allowed anyway
The only exception within strict_types concerns passing an int value to a float parameter or return type. This widening remains deliberately allowed because every integer value can be represented as a floating-point number without loss, so the conversion loses no information, unlike, say, truncating a float to an integer.
This decision follows a general principle of numeric type systems: widening, from a more precise to a less precise but more encompassing type, is lossless and therefore unproblematic. The reverse direction, a float passed to an int parameter, is not safe, though, because decimal places would be lost, which is why strict_types consistently blocks exactly this direction and instead requires an explicit conversion with (int) or intval().
| Scenario | Without strict_types (coercive) | With strict_types=1 | Consequence |
|---|---|---|---|
| String "42" passed to an int parameter | Silently converted to int 42 | TypeError is thrown | Wrong types stand out immediately |
| Float passed to an int parameter | Truncated to int, precision loss unnoticed | TypeError is thrown | No silent precision loss |
| int passed to a float parameter | Works, gets widened | Still works (allowed) | No regression on numeric widening |
| Declared return value | Converted if needed | Must match exactly (except int to float) | Function contract guaranteed |
| Bug detection during review | Error surfaces only at runtime elsewhere | Error visible directly at the call site | Shorter debugging cycles |
6. strict_types and internal PHP functions
A common misunderstanding concerns internal PHP functions like strlen() or array_map(). strict_types applies to calls of internal functions just as it does to user-defined functions, provided the file containing the call has strict_types enabled. A call strlen(42) in a strict file therefore throws a TypeError, because strlen() expects a string parameter.
What matters is the caller's file, not the file where the called function is defined. Internal functions have no file of their own in the conventional sense, their behavior depends entirely on whether the calling code is under strict_types. That means the same call to strlen(42) behaves differently in a file with strict_types than in a file without that declaration, even though strlen() itself stays unchanged.
<?php
declare(strict_types=1);
// Internal functions are checked just as strictly as user-defined ones,
// because strict_types depends on the CALLER's file, not the callee's
function describeLength(int|string $value): string
{
// Passing an int here would throw a TypeError: strlen() requires a string
return sprintf('Length: %d characters', strlen((string) $value));
}
echo describeLength('hyva');
// Throws TypeError under strict_types, since strlen() expects a string:
// strlen(42);
7. Why the effect is per-file, not project-wide
A central and often surprising aspect of strict_types: the declaration only concerns function calls made from within that particular file, not the functions defined there. A function defined in a strict file still checks its arguments strictly when it is called from a file without strict_types, because the check depends on the function's definition, not on the caller.
Conversely: if a file with strict_types calls a function from another file without that declaration, strict mode still applies to that call, because the caller is the deciding file. This combination rule occasionally causes confusion in mixed codebases, but it is consistent: whether a specific call is checked strictly is always decided by the file the call is made from, never by the file of the called function.
This per-file nature makes strict_types easy to migrate but also inconsistent within a project until every file has been converted. A legacy module without strict_types that calls a new, strict library function is still subject to the library's strictness for that call, which occasionally uncovers existing, previously unnoticed type errors in the legacy code.
8. Catching errors early: TypeError instead of silent conversion
The real value of strict_types lies in shifting the moment an error surfaces. Without this declaration, a bug often originates at a spot where a wrong type gets silently converted, but only shows up many calls later, when the silently converted value causes a visible problem somewhere else. The actual cause and the visible effect then sit far apart in the code.
With strict_types, the same bug stands out exactly at the spot where the wrong type is first passed, with a clear TypeError message naming the expected and the actual type. For debugging, that means a considerable time saving, because the stack trace points directly to the faulty call site, instead of to a later symptom that is only loosely connected to the actual root cause.
9. Migrating existing codebases to strict_types incrementally
Migrating an existing, grown codebase to strict_types should never happen in a single big step. Since the declaration is per-file, it can be introduced file by file or module by module, starting with newly written code and well-tested, isolated components, before older, less well-covered code follows. Every converted file should be accompanied by a full test suite, since previously silently working calls can suddenly start throwing errors.
Static analysis tools like PHPStan help uncover type inconsistencies before strict_types is even activated, by flagging call sites with potentially wrong types, without the code actually having to run. For new projects and new files, on the other hand, there is no reason to skip strict_types: the declaration belongs today at the top of every new PHP file, right after the opening tag, as a fixed part of the file boilerplate.
<?php
declare(strict_types=1);
// Migrated file: strict from the top, calling a not-yet-migrated helper
final class LegacyPriceAdapter
{
public function normalize(mixed $legacyValue): int
{
// Guard clause instead of relying on implicit coercion,
// since strict_types would reject a raw string or float here
if (!is_int($legacyValue)) {
throw new InvalidArgumentException(
sprintf('Expected int from legacy source, got %s', get_debug_type($legacyValue))
);
}
return $legacyValue;
}
}
10. Summary
strict_types turns off PHP's automatic, silent type conversion for the given file and instead demands exactly matching types for function arguments and return values, with the sole exception of the lossless widening from int to float. Instead of a silently converted, potentially wrong value, PHP throws a TypeError exception, right at the spot of the faulty call, not somewhere later in the program flow.
Since the declaration is per-file, the strictness of a function call always depends on the caller's file, regardless of where the called function is defined. For new PHP files, there is practically no reason to skip declare(strict_types=1);, for existing codebases a gradual, test-covered migration is recommended over a one-time, project-wide conversion.
declare(strict_types=1), the Essentials
Effect
Disables silent type conversion. TypeError instead of automatic conversion for parameters and return values.
Exception
int to float parameter remains allowed, since this widening is lossless. Every other scalar conversion is rejected.
Scope
Must be the first statement of the file. Applies to calls from this file, not project-wide and not to the defined functions themselves.
Migration
File by file with test coverage. PHPStan helps uncover type inconsistencies before activation.
11. FAQ: declare(strict_types=1)
1Where must the declaration sit in the file?
2Project-wide or per file?
3String passed to an int parameter?
4int to float parameter still allowed?
5Does it apply to strlen() and similar?
6Does the function's file or the caller's file count?
7Does it also affect return values?
8Always use it in new files?
9Safely migrate a large codebase?
10Does it make code slower?
Mironsoft
PHP architecture, code quality, and Magento development
Want to migrate an existing PHP project to strict_types incrementally?
We analyze existing code for type inconsistencies, plan a safe, test-covered migration to strict_types, and set up PHPStan as continuous coverage for new and existing files.
Type analysis
Uncovering type inconsistencies with PHPStan before migration
Incremental migration
File- and module-wise introduction of strict_types with test coverage
PHPStan coverage
Continuous static analysis at level 5 and above for new code