0

declare(strict_types=1) — why it exists and what it changes

Beginner5 min read·php-02-001
interview

Concept

declare(strict_types=1) is the single most consequential line in a PHP file. It fundamentally changes how the type system behaves for that file's function calls, converting PHP from a "helpful but dangerous" loosely-typed language into a properly type-enforced one. Every serious PHP project written in the last five years uses it. Interviewers ask about it specifically because understanding it reveals your depth of knowledge about PHP's type system.

What it changes

Without strict types, PHP performs implicit type coercion when calling functions with typed parameters. The engine tries to convert the passed value to the declared type:

  • int function receives "42" → silently converts to 42
  • float function receives 1 (int) → silently converts to 1.0
  • string function receives 42 → silently converts to "42"
  • bool function receives 1 → silently converts to true

With strict_types=1, none of these silent conversions happen. A TypeError is thrown immediately with a clear message: "must be of type int, string given."

What it does NOT change

  • It does not affect return type coercion — return types are strictly enforced regardless of the declare directive (since PHP 7.0+, return type strictness is always on)
  • It does not affect property types — typed properties enforce their types strictly regardless of strict_types
  • It does not affect built-in PHP functionsstrlen(42) still works because built-in functions always use loose mode regardless of your declare
  • It applies only to the current file's function calls, not to functions defined in other files

The file-level scoping rule

This is the gotcha that trips up experienced developers. strict_types is per-compilation-unit (per file). If FileA.php declares strict types and calls a function defined in FileB.php, the strict-ness depends on the call site (FileA.php), not the function definition site (FileB.php).

Call siteFunction definitionType checking
strict_types=1no declarationSTRICT (TypeError on mismatch)
no declarationstrict_types=1LOOSE (coercion happens)
strict_types=1strict_types=1STRICT

This means even well-written strict-type libraries can receive coerced values if called from non-strict code.

Why it matters for real codebases

In a team setting, without strict types, passing a string "0" to a function expecting bool coerces to false. Passing "" to an int function gives 0. These bugs manifest far from where the wrong type was passed, making them nightmarish to debug. Strict types make the bug location explicit and immediate.

Code Example

php
<?php
declare(strict_types=1);

// Demonstrates the difference between strict and loose mode

function calculateTax(float $amount, float $rate): float
{
    return $amount * ($rate / 100);
}

// With strict_types=1, these work:
calculateTax(100.0, 7.5);   // ✓
calculateTax(100, 7);        // ✓ — int is widened to float (the one allowed coercion)

// These throw TypeError with strict_types=1:
// calculateTax("100", "7.5");  // TypeError: must be float, string given
// calculateTax(null, 7.5);     // TypeError: must be float, null given

// The one exception: int → float widening IS allowed even in strict mode
// This is intentional — int is a subtype of float in PHP's numeric hierarchy
function taxWithInt(float $amount): float { return $amount * 0.1; }
taxWithInt(100);  // Works in strict mode — 100 (int) widened to 100.0 (float)

// Practical example: a UserService that requires strict types
final class Money
{
    public function __construct(
        private readonly int $cents,  // Store as integer cents to avoid float precision issues
        private readonly string $currency,
    ) {
        if ($this->cents < 0) {
            throw new \InvalidArgumentException("Amount cannot be negative");
        }
        if (strlen($this->currency) !== 3) {
            throw new \InvalidArgumentException("Currency must be 3-character ISO code");
        }
    }

    public function add(self $other): self
    {
        if ($this->currency !== $other->currency) {
            throw new \InvalidArgumentException("Cannot add different currencies");
        }
        return new self($this->cents + $other->cents, $this->currency);
    }

    public function format(): string
    {
        return sprintf('%s %.2f', $this->currency, $this->cents / 100);
    }
}

$price = new Money(cents: 1999, currency: 'USD');  // $19.99
$tax   = new Money(cents: 150, currency: 'USD');   // $1.50
$total = $price->add($tax);
echo $total->format(); // USD 21.49

// Without strict types, passing "1999" (string) for $cents would silently coerce.
// With strict types, it throws TypeError immediately, catching the bug at the source.

Interview Q&A

Q: Explain exactly what declare(strict_types=1) does and what it doesn't affect.

declare(strict_types=1) makes PHP enforce declared parameter types strictly for function calls originating in that file, throwing TypeError instead of silently coercing values. It applies to the call site (the file making the call), not the function definition. It does not affect built-in PHP functions (they remain loosely typed), property type enforcement (which is always strict), or return types (which are also always strict since PHP 7.0). The single allowed coercion even in strict mode is widening int to float when a function expects float — this is by design as integers are numerically a subset of floats.


Q: If you declare strict types in a library class, can a caller without strict types still pass wrong types?

Yes. Because strict types apply at the call site, not the definition. If ServiceClass.php has strict_types=1 but CallerController.php does not, and the controller calls a method on the service with a string where int is expected, PHP uses the loose rules from the call site and coerces the string to int — no TypeError. This is counterintuitive. The practical takeaway: enabling strict types project-wide is only effective if every file declares it, particularly the entry points and controllers. Running PHPStan or Psalm at level 6+ will catch these mismatches statically regardless of runtime declarations.


Q: How does strict_types interact with PHP's internal (built-in) functions?

Built-in PHP functions (those written in C and bundled with PHP) always use loose type coercion regardless of strict_types=1 in the calling file. strlen(42) will coerce 42 to "42" and return 2 even in strict mode. array_map(), implode(), in_array() — all built-ins ignore your strict declaration. This was a deliberate design decision to maintain backward compatibility. The strict mode only governs calls to user-defined PHP functions. This means if you wrap built-in calls in your own typed wrappers, you get the strictness benefits: function myStrlen(string $str): int { return strlen($str); } — calling myStrlen(42) from strict mode now throws.