Skip to content

PHP Type Safety

PHP has evolved significantly in terms of type safety, offering developers tools to write more robust and maintainable code. This guide covers type hinting, strict typing, and best practices for implementing type safety in PHP.

PHP by default performs type coercion (automatic type conversion) which can lead to unexpected behavior and hard-to-debug issues:

<?php
// Without strict types, PHP performs automatic type conversion
function calculateTotal(int $price, int $quantity): int {
return $price * $quantity;
}
// These all "work" but may not behave as expected
calculateTotal("10", "5"); // Returns 50 (strings converted to integers)
calculateTotal(10.7, 5.3); // Returns 56 (floats truncated to integers)
calculateTotal(true, false); // Returns 0 (booleans converted: true=1, false=0)
calculateTotal("10abc", "5"); // Returns 50 (string "10abc" converted to 10)
calculateTotal(null, 5); // Returns 0 (null converted to 0)

These automatic conversions can cause:

  • Silent data corruption: "10.5" becomes 10, losing precision
  • Logic errors: true becomes 1, affecting calculations
  • Security vulnerabilities: Unexpected type conversions in validation
  • Debugging nightmares: Issues may only surface in production with specific data

With strict types enabled, PHP enforces exact type matching:

<?php
declare(strict_types=1);
function calculateTotal(int $price, int $quantity): int {
return $price * $quantity;
}
// This will throw a TypeError in strict mode
calculateTotal("10", "5"); // TypeError: Argument 1 must be of type int, string given
calculateTotal(10.7, 5.3); // TypeError: Argument 1 must be of type int, float given

  • Scalar types are the basic data types in PHP: string, int, float, and bool.
  • Type hinting these parameters ensures your functions receive exactly the data types they expect, preventing unexpected behavior from automatic type conversion.
  • PHP supports type hints for scalar types since PHP 7.0:
<?php
declare(strict_types=1);
function processData( string $name, int $age, float $salary, bool $isActive ): array {
return [
'name' => $name,
'age' => $age,
'salary' => $salary,
'active' => $isActive
];
}
// Usage
$result = processData("John Doe", 30, 50000.50, true);

<?php
declare(strict_types=1);
function greetUser(?string $name): string {
if ($name === null) {
return "Hello, Guest!";
}
return "Hello, {$name}!";
}
// Both are valid
greetUser("Alice"); // "Hello, Alice!"
greetUser(null); // "Hello, Guest!"

Return type declarations specify what type of data a function will return. This helps both developers and IDEs understand function behavior and catches return type mismatches early.

<?php
declare(strict_types=1);
function calculateTax(float $amount): float {
return $amount * 0.1;
}
function getUsers(): array {
return ['user1', 'user2', 'user3'];
}
function isValid(): bool {
return true;
}
function processOrder(): void {
// Function returns nothing
echo "Order processed";
}

Union types allow a parameter or return value to accept multiple specific types using the pipe (|) operator. This provides flexibility while maintaining type safety by explicitly defining which types are acceptable.

<?php
declare(strict_types=1);
function formatValue(int|float $value): string {
return number_format($value, 2);
}
function getId(): int|string {
// Can return either int or string
return rand(0, 1) ? 123 : "ABC123";
}

Following these practices will help you write more reliable and maintainable PHP code with proper type safety implementation.

  1. Always use declare(strict_types=1) in new projects, it catches bugs early
  2. Type all function parameters and return types; it serves as documentation
  3. Be explicit about nullable types when needed:
    function findUser(int $id): ?User {
    // Returns User object or null
    }
  4. Use union types (PHP 8+) for flexibility:
    function processId(int|string $id): string {
    return (string) $id;
    }

Class type hints ensure that function parameters are instances of specific classes or implement certain interfaces. This is crucial for object-oriented programming and dependency injection patterns.

<?php
declare(strict_types=1);
class User {
public function __construct(
private string $name,
private string $email
) {}
}
class UserService {
public function saveUser(User $user): bool {
// Save user logic
return true;
}
public function getUser(int $id): User {
return new User("John Doe", "john@example.com");
}
}

<?php
declare(strict_types=1);
interface PaymentProcessorInterface {
public function processPayment(float $amount): bool;
}
class StripeProcessor implements PaymentProcessorInterface {
public function processPayment(float $amount): bool {
// Stripe-specific logic
return true;
}
}
class PaymentService {
public function __construct(
private PaymentProcessorInterface $processor
) {}
public function charge(float $amount): bool {
return $this->processor->processPayment($amount);
}
}

The mixed type accepts values of any type. It’s equivalent to combining all possible types into one union type. Use mixed when a function legitimately needs to handle multiple different types, but be cautious as it reduces type safety benefits.

<?php
declare(strict_types=1);
function handleData(mixed $data): mixed {
if (is_string($data)) {
return strtoupper($data);
}
if (is_array($data)) {
return count($data);
}
return $data;
}

The never type indicates that a function will never return normally. It either throws an exception, calls exit(), or enters an infinite loop. This helps static analysis tools understand that code after such function calls is unreachable.

<?php
declare(strict_types=1);
function throwError(): never {
throw new Exception("Something went wrong");
}
function redirect(string $url): never {
header("Location: {$url}");
exit();
}

Intersection types require a value to satisfy all specified types simultaneously using the ampersand (&) operator. This is commonly used with interfaces where an object must implement multiple interfaces.

<?php
declare(strict_types=1);
interface Readable {
public function read(): string;
}
interface Writable {
public function write(string $data): void;
}
function processFile(Readable&Writable $file): void {
$content = $file->read();
$file->write(strtoupper($content));
}

Property type declarations ensure that class properties can only hold values of specific types. This prevents accidental assignment of wrong data types and makes your classes more predictable and secure.

<?php
declare(strict_types=1);
class Product {
public string $name;
public float $price;
public ?string $description = null;
public array $categories = [];
private int $id;
protected DateTime $createdAt;
public function __construct(string $name, float $price) {
$this->name = $name;
$this->price = $price;
$this->createdAt = new DateTime();
}
}

The readonly modifier makes a property read-only, meaning it can only be assigned a value once during initialization. This prevents accidental modification of properties after object creation.

This is particularly useful for immutable objects or security-critical data.

<?php
declare(strict_types=1);
class Order {
public function __construct(
public readonly int $id,
public readonly string $customerEmail,
public readonly float $total
) {}
}
$order = new Order(1, "customer@example.com", 99.99);
// $order->id = 2; // Error: Cannot modify readonly property

When working with strict types, it’s important to handle TypeError exceptions gracefully and provide meaningful error messages to help debug type-related issues.

<?php
declare(strict_types=1);
function safelyProcessNumber(int $number): string {
try {
return "Number: " . ($number * 2);
} catch (TypeError $e) {
return "Error: Invalid type provided";
}
}
// Custom type validation
function validateAndProcess(mixed $value): int {
if (!is_int($value)) {
throw new TypeError("Expected integer, got " . gettype($value));
}
return $value * 2;
}

These comprehensive best practices will help you implement type safety effectively across your PHP projects, leading to more robust and maintainable code.

<?php
declare(strict_types=1);
// Always start your PHP files with this declaration

2. Type All Function Parameters and Returns

Section titled “2. Type All Function Parameters and Returns”
<?php
declare(strict_types=1);
// Good
function calculateDiscount(float $price, float $discountPercent): float {
return $price * ($discountPercent / 100);
}
// Avoid - no type hints
function calculateDiscount($price, $discountPercent) {
return $price * ($discountPercent / 100);
}

<?php
declare(strict_types=1);
// Good - explicit about nullable parameter
function formatName(?string $firstName, string $lastName): string {
return $firstName ? "{$firstName} {$lastName}" : $lastName;
}
// Less clear - using default parameter
function formatName(string $firstName = '', string $lastName = ''): string {
return $firstName ? "{$firstName} {$lastName}" : $lastName;
}
<?php
declare(strict_types=1);
function processId(int|string $id): string {
return is_int($id) ? "ID: {$id}" : "Code: {$id}";
}

5. Use Interface Type Hints for Dependency Injection

Section titled “5. Use Interface Type Hints for Dependency Injection”
<?php
declare(strict_types=1);
interface LoggerInterface {
public function log(string $message): void;
}
class EmailService {
public function __construct(
private LoggerInterface $logger
) {}
public function sendEmail(string $to, string $subject): bool {
$this->logger->log("Sending email to {$to}");
// Email sending logic
return true;
}
}

Learn from these common mistakes that developers make when implementing type safety in PHP, along with practical solutions to avoid them.

<?php
// Without declare(strict_types=1);
function addNumbers(int $a, int $b): int {
return $a + $b;
}
addNumbers("5", "10"); // Returns 15 (strings converted to integers)
// With declare(strict_types=1);
declare(strict_types=1);
function addNumbers(int $a, int $b): int {
return $a + $b;
}
addNumbers("5", "10"); // TypeError thrown

<?php
declare(strict_types=1);
// Inconsistent - mixing typed and untyped
class UserService {
public function createUser(string $name, $email): User { // $email should be typed
// ...
}
public function updateUser($user, string $name): bool { // $user should be typed
// ...
}
}
// Consistent - all parameters typed
class UserService {
public function createUser(string $name, string $email): User {
// ...
}
public function updateUser(User $user, string $name): bool {
// ...
}
}

<?php
declare(strict_types=1);
// Problematic - not checking for null
function processUser(?User $user): string {
return $user->getName(); // Potential null pointer error
}
// Better - proper null handling
function processUser(?User $user): string {
if ($user === null) {
return "Guest User";
}
return $user->getName();
}
// Even better - using null coalescing
function processUser(?User $user): string {
return $user?->getName() ?? "Guest User";
}

Type safety in PHP provides numerous benefits:

  • Early error detection - Catch type-related bugs at runtime rather than in production
  • Better IDE support - Enhanced autocomplete and refactoring capabilities
  • Improved documentation - Function signatures serve as documentation
  • Easier maintenance - Type hints make code intentions clearer
  • Better performance - PHP engine can optimize typed code better