Type Declarations

Type declarations in PHP let you specify exactly what kind of data your functions expect and return. While PHP's dynamic typing offers flexibility, type declarations add safety nets that catch errors early and make your code more predictable. Think of them as contracts - your code promises to work with specific data types, and PHP enforces those promises.

Modern PHP has evolved far beyond its "anything goes" reputation. Type declarations are central to this evolution, bringing the reliability of strongly-typed languages while maintaining PHP's approachable syntax. They're not just academic concepts - they solve real problems you'll encounter in every web application.

The brutal truth? Type declarations won't make bad code good, but they will make good code more reliable. They catch mistakes before they become bugs, make your intentions clearer to other developers, and help IDEs provide better autocomplete and error detection. However, they also add complexity and can make simple tasks feel heavyweight if overused.

Solving Problems from Dynamic Typing

Before diving into syntax, let's address the elephant in the room. In our previous lessons, we explored PHP's automatic type conversion - how "5" + 3 becomes 8 and how variables can change types freely. Type declarations directly address the problems this flexibility creates.

Remember this confusing behavior from our data types lesson?

The function accepts any data type and tries to make sense of it through type juggling. This flexibility is convenient until users input unexpected data, causing calculations to fail silently or produce wrong results.

Type declarations solve this by making your expectations explicit:

<?php
// Function with type declarations - only accepts specific types
function calculateDiscountTyped(float $price, float $discountPercent): float {
    $discount = $price * ($discountPercent / 100);
    return $price - $discount;
}

// Valid calls work normally
echo "Valid: $" . calculateDiscountTyped(100.0, 10.0) . "<br>";
echo "Auto-converted: $" . calculateDiscountTyped(100, 10) . "<br>";

// Invalid input gets caught - uncomment to see the error
// echo calculateDiscountTyped("abc", 10);  // Would throw TypeError
?>

The typed version still allows reasonable conversions (integers to floats) but rejects nonsensical inputs that would cause silent failures.

Understanding Type Declarations

Type declarations work by specifying the expected data type before parameter names in function signatures and after the closing parenthesis for return types. PHP checks these declarations at runtime and throws TypeError exceptions when the contract is violated.

The syntax follows this pattern: function name(type $parameter): returnType. This tells PHP "this function expects a parameter of this type and will return this type."

<?php
// Basic function with type declarations
function greetUser(string $name): string {
    return "Hello, " . $name . "!";
}

function addNumbers(int $a, int $b): int {
    return $a + $b;
}

function calculateTax(float $amount): float {
    return $amount * 0.08;
}

// Test the functions
echo greetUser("Alice") . "<br>";
echo "5 + 3 = " . addNumbers(5, 3) . "<br>";
echo "Tax on $100: $" . number_format(calculateTax(100), 2) . "<br>";
?>

PHP performs reasonable type coercion when possible. The string "5" can become the integer 5, but the string "hello" cannot. This coercion follows specific rules that balance convenience with safety.

String Type Declarations

The string type accepts text data and values that can be reasonably converted to strings. Numbers and booleans convert automatically, but arrays and objects typically don't.

<?php
function formatMessage(string $message): string {
    return "[INFO] " . $message;
}

function createSlug(string $title): string {
    $slug = strtolower($title);                    // Convert to lowercase
    $slug = str_replace(' ', '-', $slug);          // Replace spaces with hyphens
    return $slug;
}

// These conversions work automatically
echo formatMessage("User logged in") . "<br>";
echo formatMessage(404) . "<br>";          // Number converts to string
echo createSlug("My Blog Post") . "<br>";
?>

String type declarations are particularly useful for functions that manipulate text, generate output, or work with user input where you need guaranteed string operations.

Integer Type Declarations

The int type accepts whole numbers and values that can be converted to integers. Strings containing valid numbers convert automatically, but strings with non-numeric content cause type errors.

<?php
function calculateAge(int $birthYear): int {
    return 2024 - $birthYear;
}

function multiplyByTwo(int $number): int {
    return $number * 2;
}

function countItems(int $quantity): string {
    if ($quantity == 1) {
        return "1 item";
    }
    return $quantity . " items";
}

// Integer conversions and calculations
echo "Age: " . calculateAge(1990) . " years<br>";
echo "Double of 5: " . multiplyByTwo(5) . "<br>";
echo countItems(1) . "<br>";
echo countItems(5) . "<br>";
?>

Integer type declarations excel in mathematical calculations, counting operations, and any scenario where fractional values would be meaningless or harmful.

Float Type Declarations

The float type accepts decimal numbers and converts integers and numeric strings automatically. This type is essential for calculations involving money, measurements, or scientific data.

<?php
function calculateInterest(float $principal, float $rate): float {
    return $principal * $rate;
}

function convertCelsiusToFahrenheit(float $celsius): float {
    return ($celsius * 9/5) + 32;
}

function calculateTip(float $billAmount, float $tipPercent): float {
    return $billAmount * ($tipPercent / 100);
}

// Float calculations
$interest = calculateInterest(1000, 0.05);
echo "Interest: $" . number_format($interest, 2) . "<br>";

$fahrenheit = convertCelsiusToFahrenheit(25);
echo "25°C = " . number_format($fahrenheit, 1) . "°F<br>";

$tip = calculateTip(50.00, 18);
echo "Tip on $50: $" . number_format($tip, 2) . "<br>";
?>

Boolean Type Declarations

The bool type accepts true/false values and converts other types according to PHP's truthiness rules. Empty strings, zero, null, and empty arrays become false; everything else becomes true.

<?php
function isPasswordValid(string $password): bool {
    return strlen($password) >= 8;
}

function canVote(int $age): bool {
    return $age >= 18;
}

function shouldApplyDiscount(bool $isPremium, float $orderAmount): bool {
    return $isPremium || $orderAmount > 100;
}

?>

Array Type Declarations

The array type declaration accepts any PHP array but doesn't specify what the array should contain. This provides basic type safety for the container but not the contents.

<?php
function calculateAverage(array $numbers): float {
    if (empty($numbers)) {
        return 0.0;
    }

    $sum = array_sum($numbers);
    $count = count($numbers);
    return $sum / $count;
}

function joinWords(array $words): string {
    return implode(" ", $words);
}

function countElements(array $items): int {
    return count($items);
}

// Array processing examples
$testScores = [85, 92, 78, 96, 88];
echo "Average score: " . number_format(calculateAverage($testScores), 1) . "<br>";

$sentence = ["Hello", "world", "from", "PHP"];
echo joinWords($sentence) . "<br>";

echo "Array has " . countElements($testScores) . " elements<br>";
?>

Nullable Types

Real-world applications frequently deal with missing or optional data. Nullable types allow parameters and return values to accept either the specified type or null, handling these scenarios gracefully.

The syntax uses a question mark (?) before the type name: ?string, ?int, ?float, etc.

<?php
function formatUserName(?string $firstName, ?string $lastName): string {
    if ($firstName === null && $lastName === null) {
        return "Anonymous User";
    }

    if ($firstName === null) {
        return $lastName;
    }

    if ($lastName === null) {
        return $firstName;
    }

    return $firstName . " " . $lastName;
}

function findNumber(array $numbers, int $target): ?int {
    foreach ($numbers as $index => $number) {
        if ($number === $target) {
            return $index;  // Return the position where found
        }
    }
    return null;  // Not found
}

// Nullable type examples
echo formatUserName("John", "Doe") . "<br>";
echo formatUserName("Jane", null) . "<br>";
echo formatUserName(null, "Smith") . "<br>";
echo formatUserName(null, null) . "<br>";

$numbers = [10, 20, 30, 40];
$position = findNumber($numbers, 30);
echo "Found 30 at position: " . ($position !== null ? $position : "Not found") . "<br>";

$notFound = findNumber($numbers, 99);
echo "Found 99: " . ($notFound !== null ? "Yes" : "Not found") . "<br>";
?>

Union Types (PHP 8.0+)

Union types allow parameters and return values to accept multiple specific types, providing flexibility while maintaining type safety. The syntax uses the pipe symbol (|) to separate allowed types.

<?php
function formatId(string|int $id): string {
    if (is_string($id)) {
        return strtoupper($id);
    }
    return "ID" . $id;
}

function doubleValue(int|float $number): int|float {
    return $number * 2;
}

function processData(array|string $input): string {
    if (is_array($input)) {
        return "Array with " . count($input) . " items";
    }
    return "String: " . $input;
}

// Union type examples
echo formatId("abc123") . "<br>";
echo formatId(42) . "<br>";

echo "Double 5: " . doubleValue(5) . "<br>";
echo "Double 2.5: " . doubleValue(2.5) . "<br>";

echo processData(["a", "b", "c"]) . "<br>";
echo processData("Hello World") . "<br>";
?>

Mixed Type

The mixed type explicitly indicates that a parameter or return value can be any type. While this might seem to defeat the purpose of type declarations, it serves important documentation purposes.

<?php
function describeValue(mixed $value): string {
    $type = gettype($value);

    if (is_string($value)) {
        return "String: '$value'";
    }

    if (is_int($value)) {
        return "Integer: $value";
    }

    if (is_float($value)) {
        return "Float: $value";
    }

    if (is_bool($value)) {
        return "Boolean: " . ($value ? 'true' : 'false');
    }

    if (is_array($value)) {
        return "Array with " . count($value) . " elements";
    }

    return "Type: $type";
}

// Mixed type examples
echo describeValue("Hello") . "<br>";
echo describeValue(42) . "<br>";
echo describeValue(3.14) . "<br>";
echo describeValue(true) . "<br>";
echo describeValue([1, 2, 3]) . "<br>";
echo describeValue(null) . "<br>";
?>

Practical Example: Simple Calculator

Let's apply these concepts in a realistic scenario with multiple functions working together:

<?php
function add(int|float $a, int|float $b): int|float {
    return $a + $b;
}

function subtract(int|float $a, int|float $b): int|float {
    return $a - $b;
}

function multiply(int|float $a, int|float $b): int|float {
    return $a * $b;
}

function divide(int|float $a, int|float $b): ?float {
    if ($b == 0) {
        return null;  // Cannot divide by zero
    }
    return $a / $b;
}

function formatResult(string $operation, mixed $result): string {
    if ($result === null) {
        return "$operation = Error (division by zero)";
    }

    if (is_float($result)) {
        return "$operation = " . number_format($result, 2);
    }

    return "$operation = $result";
}

// Test the calculator functions
echo formatResult("10 + 5", add(10, 5)) . "<br>";
echo formatResult("10.5 - 3.2", subtract(10.5, 3.2)) . "<br>";
echo formatResult("6 × 7", multiply(6, 7)) . "<br>";
echo formatResult("15 ÷ 3", divide(15, 3)) . "<br>";
echo formatResult("10 ÷ 0", divide(10, 0)) . "<br>";
?>

This example demonstrates multiple type declaration concepts working together in a practical scenario that a beginner can understand.

When NOT to Use Type Declarations

Type declarations aren't always the right choice. Here are scenarios where they might be counterproductive:

Simple utility functions that work with any data type:

<?php
// These functions work fine without type declarations
function debug($value) {
    echo "<pre>" . print_r($value, true) . "</pre>";
}

function isEmpty($value) {
    return empty($value);
}

// Test the flexible functions
debug("Hello World");
debug([1, 2, 3]);

echo "Empty string: " . (isEmpty("") ? "true" : "false") . "<br>";
echo "Empty array: " . (isEmpty([]) ? "true" : "false") . "<br>";
echo "Number zero: " . (isEmpty(0) ? "true" : "false") . "<br>";
?>

Rapid prototyping where you're still exploring the problem space and requirements might change frequently.

Best Practices

Following these practices makes your code more maintainable and reduces bugs:

Start Simple

Add type declarations to functions where they provide clear benefits - catching errors, documenting intent, or preventing common mistakes.

<?php
// Good candidate for type declarations - prevents calculation errors
function calculateTotalPrice(float $price, int $quantity, float $taxRate): float {
    $subtotal = $price * $quantity;
    $tax = $subtotal * $taxRate;
    return $subtotal + $tax;
}

// Simple function - types optional but still helpful
function greet(string $name): string {
    return "Welcome, " . $name . "!";
}

$total = calculateTotalPrice(19.99, 3, 0.08);
echo "Total: $" . number_format($total, 2) . "<br>";
echo greet("Alice") . "<br>";
?>

Use Nullable Types for Optional Data

When data might legitimately be missing, use nullable types instead of forcing artificial default values.

<?php
function createGreeting(?string $name, ?string $timeOfDay): string {
    $greeting = "Hello";

    if ($timeOfDay !== null) {
        $greeting = match($timeOfDay) {
            'morning' => 'Good morning',
            'afternoon' => 'Good afternoon',
            'evening' => 'Good evening',
            default => 'Hello'
        };
    }

    if ($name !== null) {
        $greeting .= ", " . $name;
    }

    return $greeting . "!";
}

// Test with various combinations
echo createGreeting("Alice", "morning") . "<br>";
echo createGreeting("Bob", null) . "<br>";
echo createGreeting(null, "evening") . "<br>";
echo createGreeting(null, null) . "<br>";
?>

Validate Complex Requirements

Type declarations only verify basic type categories. For complex requirements, combine type declarations with validation logic.

<?php
function calculateGrade(int $score): string {
    // Type declaration ensures we have an integer
    // But we still need to validate the range
    if ($score < 0 || $score > 100) {
        return "Invalid score (must be 0-100)";
    }

    if ($score >= 90) return "A";
    if ($score >= 80) return "B";
    if ($score >= 70) return "C";
    if ($score >= 60) return "D";
    return "F";
}

// Test grade calculation
echo "Score 95: " . calculateGrade(95) . "<br>";
echo "Score 75: " . calculateGrade(75) . "<br>";
echo "Score 55: " . calculateGrade(55) . "<br>";
echo "Invalid score: " . calculateGrade(150) . "<br>";
?>

Common Pitfalls

Understanding frequent mistakes helps you use type declarations effectively:

Over-specification

Don't declare types just because you can. Use them where they provide clear benefits.

<?php
// This might be over-specified for a simple function
function sayHello(string $name): string {
    return "Hello " . $name;
}

// This benefits more from type declarations due to calculations
function calculateMonthlyPayment(float $amount, float $interestRate, int $months): float {
    if ($months <= 0) {
        return 0.0;
    }

    $monthlyRate = $interestRate / 12;
    return $amount * ($monthlyRate / (1 - pow(1 + $monthlyRate, -$months)));
}

echo sayHello("World") . "<br>";
$payment = calculateMonthlyPayment(20000, 0.05, 60);
echo "Monthly payment: $" . number_format($payment, 2) . "<br>";
?>

Forgetting About Null

Functions that might not find results should return nullable types rather than forcing artificial values.

<?php
// Poor approach - empty array doesn't clearly indicate "not found"
function findItemBad(array $items, string $search): array {
    foreach ($items as $item) {
        if ($item === $search) {
            return [$item];
        }
    }
    return [];  // Unclear if this means "not found" or "empty result"
}

// Better approach - null clearly indicates "not found"
function findItem(array $items, string $search): ?string {
    foreach ($items as $item) {
        if ($item === $search) {
            return $item;
        }
    }
    return null;  // Clearly indicates "not found"
}

$fruits = ["apple", "banana", "orange"];

$found = findItem($fruits, "banana");
echo "Looking for banana: " . ($found !== null ? "Found '$found'" : "Not found") . "<br>";

$notFound = findItem($fruits, "grape");
echo "Looking for grape: " . ($notFound !== null ? "Found '$notFound'" : "Not found") . "<br>";
?>

Moving Forward

Type declarations represent PHP's evolution toward more reliable, maintainable code while preserving the language's approachable nature. They're not about making PHP "more like other languages" - they're about giving developers tools to write better PHP.

As you progress through this course, you'll see type declarations integrated naturally into more advanced concepts. They become particularly valuable when building larger applications where clear interfaces and early error detection prevent debugging nightmares.

The key insight is that type declarations are optional tools, not mandatory restrictions. Use them where they add value - catching errors, documenting intent, enabling better tooling - and skip them where they add unnecessary complexity.

In our next lesson, we'll explore input and output operations along with string manipulation. You'll see how type declarations help when processing user input and generating dynamic content - core requirements for any web application.

The foundation you're building now with variables, data types, and type declarations will support everything else you learn in PHP. Take time to practice these concepts thoroughly. They're not just academic exercises - they're practical tools you'll use in every professional PHP project.

← Previous Lesson: Variables and Constants Next Lesson: Personal Information Card →