Eduardo Arsand

PHP Typed Arrays

50

PHP's native type system stops at declaring array without describing its contents.

PHPDoc annotations fill this gap through a formal syntax that static analyzers interpret. The annotations document intent while enabling machine verification of type contracts.

This note catalogs the array typing patterns I extracted from PHPStan documentation and practical implementation.

Each pattern addresses specific structural requirements: homogeneous collections, sequential lists, structured shapes, and constraint-based refinements.

Basic Array Type Notation

The foundational syntax takes three forms: Type[], array<Type>, and array<KeyType, ValueType>.

The first two specify value types only, defaulting to integer keys.

<?php
    /**
     * @param User[] $users
     * @return array<string>
     */
    function extractNames(array $users): array {
        return array_map(fn($u) => $u->getName(), $users);
    }

    /**
     * @param array<string, int> $scores
     */
    function calculateAverage(array $scores): float {
        return array_sum($scores) / count($scores);
    }
?>

Lists: Sequential Integer Keys

Lists represent arrays where keys start at 0 and increment by 1.

The distinction affects JSON serialization behavior and guarantees sequential structure.

<?php
    /**
     * @param list<int> $numbers
     * @return non-empty-list<int>
     */
    function ensureNonEmpty(array $numbers): array {
        return $numbers ?: [0];
    }
?>

Array Shapes: Structured Data

Array shapes define exact key-value contracts.

I use them extensively for configuration arrays, API responses, and database row structures.

<?php
    /**
     * @param array{name: string, age: int, email?: string} $user
     * @return array{id: int, name: string, created_at: string}
     */
    function createUser(array $user): array {
        //Optional email key validated by static analysis
        return [
            'id' => $this->generateId(),
            'name' => $user['name'],
            'created_at' => date('Y-m-d H:i:s')
        ];
    }
?>

Managing Complex Array Shapes

There are situations where array shapes exceeded readable line lengths.

Inline documentation with 10+ keys becomes unmaintainable noise. Break them into several lines.

<?php
    /**
     * @param array{
     *     id: int,
     *     name: string,
     *     email: string,
     *     phone?: string,
     *     address: array{street: string, city: string, zip: string},
     *     preferences: array{notifications: bool, theme: string},
     *     metadata: array<string, mixed>
     * } $data
     * @return bool
     */
    function saveUser(array $data): bool {
        //Code to save user data to the database
        return $isDataSaved;
    }
?>

Also, local type aliases help solve complex array shapes being declared everywhere through the @phpstan-type annotation.

<?php
    /**
     * @phpstan-type UserData array{
     *     id: int,
     *     name: string,
     *     email: string,
     *     phone?: string,
     *     address: array{street: string, city: string, zip: string},
     *     preferences: array{notifications: bool, theme: string},
     *     metadata: array<string, mixed>
     * }
     */
    class UserRepository {
        /**
         * @param UserData $data
         * @return UserData
         */
        public function save(array $data): array {
            //Type alias maintains readability
        }
    }
?>

Type aliases can be imported across classes using @phpstan-import-type. You can centralize shared shapes in dedicated type definition classes.

<?php
    /**
     * @phpstan-import-type UserData from UserRepository
     * @phpstan-import-type OrderData from OrderRepository as Order
     */
    class InvoiceService {
        /**
         * @param UserData $user
         * @param Order $order
         */
        public function generate(array $user, array $order): void {
            //Both imported types available
        }
    }
?>

This pattern separates type contracts from implementation logic.

The type definition becomes a reusable contract, preventing drift when multiple functions share the same structure.

Advanced Type Constraints

Type refinements eliminate runtime validation.

non-empty-array<T> guarantees presence. key-of<T> and value-of<T> extract types from constants.

<?php
    class Status {
        public const TYPES = [
            'pending' => 1,
            'active' => 2,
            'completed' => 3
        ];
    }

    /**
     * @param key-of<Status::TYPES> $status
     * @param value-of<Status::TYPES> $code
     */
    function updateStatus(string $status, int $code): void {
        //$status is 'pending'|'active'|'completed'
        //$code is 1|2|3
    }
?>

This approach shifts type validation from runtime to analysis time, reducing defensive programming overhead while improving IDE autocomplete accuracy.


Comments ({{ modelContent.total_comments }})