How PHP Type Declarations Coercion and Strict Types Behave
PHP type declarations control how values flow through code: strict mode throws TypeErrors on mismatch, while coercive mode silently converts where possible.
How PHP Type Declarations Coercion and Strict Types Behave
PHP will silently convert your integer into a string and carry on executing unless you tell it otherwise. One line at the top of a file, declare(strict_types=1);, turns off the automatic type coercion that has been the default for decades. Without that line, PHP runs in coercive mode and reshapes values to fit a function signature. With it, a mismatch throws a TypeError and stops the program dead. That single declaration is the difference between debugging a data corruption you never saw coming and catching the bug the moment it happens.
PHP Types Declarations Strict Types
The declare line must sit as the first instruction in a file, after the opening tag. PSR-12 requires that formatting. Put it anywhere else and the engine issues a parse error. Strict mode is file-level, not global. A file with declare(strict_types=1); enforces strict checking only on calls to routines defined within that file. Code outside the file, including code in included scripts that lack the declaration, continues to use coercive mode. Convert a legacy codebase file by file without breaking code you cannot yet rewrite.
In coercive mode, the default when no declare line exists, PHP applies a set of type juggling rules to incoming arguments and return values. An integer passed where a string is expected becomes a string. A string containing a numeric value passed where an integer is expected becomes that integer, or zero if the string does not start with a number. The coercion is predictable but lossy, and the loss is silent. A float passed where an integer is expected loses its fractional part with no warning. In strict mode, a float passed to an int parameter throws a TypeError immediately. The stack trace points to the exact call site.
PHP Type Coercion Rules
Scalar Type Casting In Coercive Mode
Coercive mode follows a specific set of casting rules. The PHP.net manual documents the authoritative runtime behaviour. When a value reaches a typed parameter in coercive mode, the engine applies the same conversion as an explicit cast to that type. For scalar types introduced in PHP 7.0, the rules are straightforward but carry edge cases that break silently. Passing the integer zero to a bool parameter yields false. Passing a non-empty string to a bool parameter yields true. Passing an empty string or the string "0" yields false. These rules match the behaviour of the (bool) cast and are consistent across versions.
Integer And Float Conversion Pitfalls
Integer coercion from a float truncates toward zero. The float 3.99 becomes the integer 3, not 4. A float too large to fit in an integer range becomes undefined in PHP 7 and a TypeError in PHP 8. String-to-integer coercion reads leading numeric characters and ignores the rest. The string "42 apples" becomes the integer 42. The string "apples 42" becomes zero, because no leading numeric characters exist. A non-numeric string passed where an integer is expected emits an E_WARNING in PHP 7.1 and later, even in coercive mode, though the value still converts to zero. That warning is the only signal that a conversion did not succeed as expected.
Return Type Coercion And Void
Return type coercion follows the same rules as parameter type coercion. If a routine declares a return type of int and returns a string like "7", coercive mode converts it to the integer 7. In strict mode, that return line throws a TypeError. The void return type, added in PHP 7.1, does not accept coercion. A routine with a void return type must not use a return line with a value, period. Returning null from a void routine throws a TypeError in all modes.
PHP Union Types Explained
Union types arrived in PHP 8.0 and allow a parameter, return value, or property to accept values of two or more types. The syntax separates types with a vertical bar: int|string. The engine checks the value against each type in the union in order and accepts the value if it matches at least one. Union types do not include false as a standalone pseudo-type but can include false as part of a union since PHP 8.0. A routine can declare string|false as a return type to signal failure without resorting to an exception or a sentinel value. Before PHP 8.0, the only way to signal a missing result from a typed routine was to return null and use the nullable type syntax.
The mixed type, also introduced in PHP 8.0, is effectively a union of every possible type. Declaring a parameter as mixed is functionally identical to omitting the type declaration entirely, but it signals intent. Code that declares mixed tells a reader and a static analyzer that the routine is designed to accept any value. The mixed type is the top type in the PHP type system. The never type, added in PHP 8.1, is the bottom type. A routine that declares a never return type must not return at all. It must either throw an exception or call exit(). The never type is useful for routines that unconditionally terminate execution, such as redirect dispatchers or fatal error handlers.
Intersection types arrived in PHP 8.1 and require a value to satisfy multiple type constraints simultaneously. The syntax uses an ampersand: Countable&Iterator. A parameter typed this way accepts only objects that implement both the Countable and Iterator interfaces. Intersection types work only with class and interface types, not with scalar types. You cannot write int&string because no value can be both. The engine rejects intersection types containing mixed, void, or never. Intersection types are useful for dependency injection where a routine needs an object that provides multiple capabilities without relying on a single monolithic interface.
PHP Mixed Type vs No Type
A parameter with no type declaration accepts any value and provides no information to the reader or to static analysis tools. A parameter declared mixed accepts any value but explicitly states that the routine expects to handle multiple types. The runtime behaviour is identical in both cases. The difference is in tooling and intent. Static analyzers like Psalm and PHPStan treat an untyped parameter as an error in strict analysis mode. They treat a mixed parameter as a signal that the type is intentionally broad and requires a type check before use. Code written with mixed declarations passes stricter CI checks without suppressing warnings.
The mixed type cannot be combined in a union because it already includes every type. Writing mixed|int is redundant and the engine rejects it with a fatal error. The mixed type cannot be used in an intersection type for the same reason. The mixed type includes null, so a mixed parameter does not need a nullable marker. Calling a routine with a mixed parameter in strict mode passes the value as-is without coercion because any value matches the type.
PHP Nullable Type Syntax
Nullable Parameters And Defaults
A nullable type declaration accepts either the named type or null. The syntax prefixes the type name with a question mark: ?string. This is syntactic sugar for string|null, which is the union type syntax available since PHP 8.0. The question-mark syntax predates union types and works in PHP 7.1 and later. Both forms behave identically at runtime. A nullable parameter with no default value requires a null argument to be passed explicitly unless the parameter has a default value of null, in which case the argument can be omitted.
Standalone Null And Property Types
Standalone null as a type arrived in PHP 8.2. Before that version, null could appear only as part of a union or as the nullable marker. In PHP 8.2 and later, you can declare a parameter or return type as null alone, which means the routine accepts or returns only null and nothing else. A standalone null return type is a stronger signal than void because void forbids any return line with a value, while null requires an explicit return null; line. The difference matters for interface contracts where a method must explicitly return null rather than return nothing.
Property type declarations support all the same syntax as parameter and return types, including union types, intersection types, nullable types, and the mixed type. A typed property that is not initialized before the constructor finishes throws a TypeError in PHP 7.4 and later, unless the property has a default value or is declared nullable without a default. Uninitialized state is distinct from null. A property typed as ?string that is never assigned has the value uninitialized, not null, and reading it before assignment throws an error. This behaviour catches a whole class of bugs where code accesses a property before the constructor runs.
Failure Behaviour And TypeError Exceptions
Strict Mode vs Coercive Mode Failures
When strict mode is on and a type mismatch occurs, PHP throws a TypeError. The message states the file, line number, and the expected and actual types. If the mismatch is in a parameter, the routine never executes. The call stack unwinds to the nearest catch block or, if uncaught, terminates the request with a fatal error. In coercive mode, the same mismatch does not throw. The value is converted to the expected type using the coercion rules, and the routine proceeds with the converted value. The only case where coercive mode throws is when the coercion itself fails, such as passing a non-numeric string to an int parameter, which emits an E_WARNING in PHP 7.1 through 8.x but still produces a zero.
Catching TypeErrors Correctly
A TypeError in PHP 8.x extends Error, not Exception, which means a generic catch (Exception $e) block will not catch it. Code that needs to handle type errors must catch TypeError specifically or catch Throwable, which is the root interface for both exceptions and errors since PHP 7.0. The distinction matters in frameworks that wrap request handling in an exception handler. A TypeError from a controller parameter in strict mode bypasses a generic exception handler unless the handler catches Throwable.
Return Type Mismatches And Side Effects
Return type mismatches in strict mode throw a TypeError from inside the routine after the return line executes but before the value reaches the caller. Side effects from the routine body have already occurred. A routine that writes to a database and then returns the wrong type will commit the database write and then throw. The caller never receives a return value, but the side effect persists. This is a common source of bugs when converting a coercive-mode codebase to strict mode file by file. Routines that relied on coercion to convert a return value may now throw after performing irreversible work.
Common Questions
Does declare(strict_types=1) apply to included files?
No. Strict mode applies only to the file that contains the declare line. Calls made from that file to routines defined elsewhere are checked strictly if the called routine is defined in a strict-mode file. Calls from other files into a strict-mode file use the caller's mode.
What happens when I pass a float to an int parameter in strict mode?
A TypeError is thrown immediately, and the routine body does not execute. In coercive mode, the float is truncated toward zero, losing the fractional part silently.
Can I use union types with intersection types together?
Yes, in PHP 8.2 and later. A parameter can be typed as (Countable&Iterator)|null, meaning it accepts an object implementing both interfaces or null. Parentheses are required to disambiguate the union and intersection boundaries.
How does the never type differ from void?
A void routine returns nothing and cannot use a return line with a value. A never routine must not return at all. It must throw an exception or call exit(). The never type signals unconditional termination and is checked at the engine level.