PHP Typed Arrays
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.