Skip to content
16

Arrays & Shapes

TypePHP provides runtime enforcement for sequential lists, key-value generic maps, typed class arrays, positional tuples, sealed and unsealed array shapes, key/value extractions, offset access, and object shapes.


Sequential Lists (list<T> & non-empty-list<T>)

A list<T> represents a sequential, 0-indexed integer key array without gaps. TypePHP validates lists at runtime using PHP's native array_is_list() function:

php
<?php

declare(strict_types=1);

/**
 * @param list<non-empty-string> $tags
 * @param non-empty-list<positive-int> $scores
 */
function processList(array $tags, array $scores): void
{
    // ...
}

// 1. Valid Call
processList(['php', 'pest', 'typephp'], [10, 20, 30]);

// 2. Invalid Call (Associative array passed where list was expected)
processList(['tag1' => 'php'], [10, 20]);
// Throws: TypeError: processList(): Argument $tags must be a list

// 3. Invalid Call (Empty array passed where non-empty-list was expected)
processList(['php'], []);
// Throws: TypeError: processList(): Argument $scores must be a non-empty list

Array Validation Strategies (full vs hybrid)

TypePHP provides two collection validation strategies configured in typephp.php:

php
// typephp.php
return [
    'array_validation' => 'full', // 'full' (default) or 'hybrid'
];

1. Strict Full Mode ('array_validation' => 'full', Default)

  • Exhaustive O(n) Check: Validates every single element in the collection regardless of size.
  • 100% Deterministic Guarantee: Guarantees that any offending item anywhere in an array will trigger an immediate TypeError.
  • Zero-Allocation Happy Path: Context path strings are constructed only when a validation check fails, ensuring zero throwaway string allocations in memory during valid iterations.

2. Beartype Hybrid Mode ('array_validation' => 'hybrid')

  • Small Collections ($N \le 128$ items): Executes a 100% full scan.
  • Large Collections ($N > 128$ items): Executes O(1) constant-time sampling:
    1. Shallow check via native array_is_list() in C.
    2. Boundary checks on the first element ($arr[0]) and last element ($arr[count - 1]).
    3. Random walk sampling across 3 internal elements.
  • Reduces validation latency on massive 100,000-item arrays from 81 seconds to 0.83 seconds (97x faster).

Key-Value Generic Arrays (array<K, V> & T[])

TypePHP enforces specific key and value types on associative or indexed arrays:

Generic Key-Value Arrays (array<K, V>)

php
/**
 * @param array<string, positive-int> $userScores
 */
function recordScores(array $userScores): void
{
    // ...
}

// 1. Valid Call
recordScores(['alice' => 100, 'bob' => 95]);

// 2. Invalid Call (Key 0 is integer instead of string)
recordScores([0 => 100]);
// Throws: TypeError: recordScores(): Argument $userScores key must be of type string

// 3. Invalid Call (Value -5 violates positive-int)
recordScores(['alice' => -5]);
// Throws: TypeError: recordScores(): Argument $userScores['alice'] must be of type positive-int

Associative Hybrid Validation: In hybrid mode, associative arrays validate both the key and the value on the first pair, the last pair, and 3 random internal pairs in O(1) time.

Typed Scalar, Refinement, & Callable Arrays (positive-int[], non-empty-string[], callable[])

In addition to typed class arrays (User[]), TypePHP validates arrays of primitives, scalar refinements, callables, or shapes using T[] syntax:

php
/**
 * @param positive-int[] $ids
 * @param non-empty-string[] $tags
 * @param callable[] $callbacks
 * @param array{id: positive-int}[] $userShapes
 */
function processTypedArrays(array $ids, array $tags, array $callbacks, array $userShapes): void
{
    // ...
}

// Valid Call
processTypedArrays(
    ids: [10, 20, 30],
    tags: ['php', 'pest'],
    callbacks: [fn () => null, 'strlen'],
    userShapes: [['id' => 1], ['id' => 2]]
);

// Invalid Call (-50 violates positive-int[])
processTypedArrays(
    ids: [10, -50, 30],
    tags: ['php', 'pest'],
    callbacks: [fn () => null],
    userShapes: [['id' => 1]]
);
// Throws: TypeError: processTypedArrays(): Argument $ids[1] must be of type positive-int, negative int (-50) given

WeakMap Object Memoization: When validating arrays of objects (such as User[]), TypePHP memoizes previously checked object instances in a \WeakMap. If the same object instance appears multiple times in a collection, its type is checked once and retrieved in $O(1)$ time on subsequent accesses.


Deeply Nested Arrays & Lists (array<K, list<V>>)

TypePHP recursively validates deeply nested array structures down to any depth:

php
/**
 * @param array<string, list<positive-int>> $matrix
 */
function processMatrix(array $matrix): void
{
    // ...
}

// 1. Valid Call
processMatrix([
    'math' => [100, 95],
    'science' => [88, 92],
]);

// 2. Invalid Call (Nested list item -50 violates positive-int)
processMatrix([
    'math' => [100, -50],
]);
// Throws: TypeError: processMatrix(): Argument $matrix['math'][1] must be of type positive-int

Multi-Level Random Walk in Hybrid Mode

In hybrid mode, nested structures compose into a multi-level random walk down the type tree. For example, a $10,000 \times 10,000$ matrix ($100,000,000$ total elements) validates in 25 element checks (0.005 ms) rather than evaluating 100 million items in userland loops.


Generics inside Typed Arrays & Shapes (list<Producer<T>>)

TypePHP validates generic container objects nested inside arrays or array shapes:

Deep Dive Guide: For comprehensive details on generic collections and variance modifiers (covariant/contravariant), see the Generics Basics & Bounds documentation.

php
use App\Generics\Producer;
use App\Models\Dog;
use App\Models\Car;

/**
 * @param list<Producer<Dog>> $producers
 * @param array{items: list<Producer<covariant Animal>>, count: positive-int} $payload
 */
function processGenericList(array $producers, array $payload): void
{
    // ...
}

// 1. Valid Call
processGenericList(
    [new Producer(new Dog()), new Producer(new Dog())],
    ['items' => [new Producer(new Dog())], 'count' => 1]
);

// 2. Invalid Call (Producer holds Car instead of Dog)
processGenericList(
    [new Producer(new Dog()), new Producer(new Car())],
    ['items' => [new Producer(new Dog())], 'count' => 1]
);
// Throws: TypeError: processGenericList(): Argument $producers[1] must be an instance of Producer<Dog>

Array Shapes (array{key: type})

Array shapes define exact key-value contracts for associative arrays.

Required vs. Optional Keys

Mark optional keys with a question mark (key?: type):

php
/**
 * @param array{id: positive-int, username: non-empty-string, role?: 'admin'|'user'} $payload
 */
function saveUserPayload(array $payload): void
{
    // ...
}

// 1. Valid Call (Optional 'role' key omitted)
saveUserPayload(['id' => 10, 'username' => 'Alice']);

// 2. Valid Call (Optional 'role' key provided)
saveUserPayload(['id' => 10, 'username' => 'Alice', 'role' => 'admin']);

// 3. Invalid Call (Missing required 'username' key)
saveUserPayload(['id' => 10]);
// Throws: TypeError: saveUserPayload(): Argument $payload is missing required key 'username'

Sealed vs. Unsealed Shapes

By default, array shapes are sealed. Any unexpected extra keys in the array will trigger a TypeError.

To allow additional dynamic keys, define an unsealed shape using ...<K, V> syntax:

php
/**
 * Unsealed Shape: Requires 'id', but permits additional string-string pairs
 *
 * @param array{id: positive-int, ...<string, string>} $options
 */
function processUnsealedOptions(array $options): void
{
    // ...
}

// 1. Valid Call (Includes extra string key 'category')
processUnsealedOptions(['id' => 10, 'category' => 'admin']);

// 2. Invalid Call (Extra key 'code' has integer value 999 instead of string)
processUnsealedOptions(['id' => 10, 'code' => 999]);
// Throws: TypeError: processUnsealedOptions(): Argument $options['code'] must be of type string

Memory Optimization for Sealed Shapes

ArrayShapeValidator uses an O(1) key count check ($valueCount === $matchedKeysCount) to verify sealed shapes without allocating array_diff_key() arrays in memory, sustaining throughput over 130,000 operations per second.

Unsealed Shapes with Complex Nested Types (...<string, list<T>> or ...<string, array{...}>)

Wildcard extra keys in unsealed shapes can be constrained to complex nested structures like lists or sub-shapes:

php
/**
 * Unsealed shape requiring 'id', but permitting extra keys holding list<positive-int>
 *
 * @param array{id: positive-int, ...<string, list<positive-int>>} $payload
 */
function processBatchOptions(array $payload): void
{
    // ...
}

// 1. Valid Call
processBatchOptions([
    'id' => 10,
    'even_scores' => [2, 4, 6],
    'odd_scores' => [1, 3, 5],
]);

// 2. Invalid Call (-3 violates positive-int in nested extra list)
processBatchOptions([
    'id' => 10,
    'odd_scores' => [1, -3, 5],
]);
// Throws: TypeError: Argument $payload['odd_scores'][1] must be of type positive-int

Positional Tuple Shapes (array{0: T1, 1: T2} & Keyless Tuples)

Define fixed-length, positional array tuples:

php
/**
 * Positional Tuple with Optional Trailing Element
 *
 * @param array{0: positive-int, 1: non-empty-string, 2?: bool} $tuple
 */
function processTuple(array $tuple): void
{
    // ...
}

// 1. Valid Call (Optional index 2 omitted)
processTuple([100, 'success']);

// 2. Valid Call (Optional index 2 provided)
processTuple([100, 'success', true]);

// 3. Invalid Call (Index 0 is negative integer)
processTuple([-5, 'success']);
// Throws: TypeError: processTuple(): Argument $tuple['0'] must be of type positive-int

Implicit Keyless Tuple Syntax (array{T1, T2})

TypePHP fully supports implicit keyless tuple syntax (e.g. array{list<positive-int>, non-empty-string}):

php
/**
 * @param array{list<positive-int>, non-empty-string} $bundle
 */
function processBundle(array $bundle): void
{
    // ...
}

processBundle([[10, 20], 'bundle_tag']); // Valid

Key & Value Extraction (key-of<T> & value-of<T>)

TypePHP supports dynamically restricting function parameters, return types, property writes, or array shape fields to the keys or values of an array constant, an array shape, or a PHP 8.1 Enum using key-of<T> and value-of<T> type operators.

Performance & Visibility: TypePHP caches array and enum extractions in static memory, guaranteeing $O(1)$ constant lookup times during execution. Furthermore, it safely resolves private and protected class constants (e.g. key-of<self::PRIVATE_MAP>) without visibility violations.

AnnotationSupported Targets TValidation Rule
key-of<T>Array Constant, Array Shape, UnitEnum, BackedEnumValidates that the value matches a valid array key, shape key, or Enum case name (e.g. 'Active', 'Hearts').
value-of<T>Array Constant, BackedEnumValidates that the value matches a valid array value or BackedEnum backing value (e.g. 'active', 1).

1. Extracting from Class Constants

Extract allowed keys or values directly from public, protected, or private class constant arrays:

php
namespace App\Database;

class DriverManager
{
    private const DRIVER_MAP = [
        'pdo_mysql'  => 'PDO\MySQL\Driver',
        'pdo_sqlite' => 'PDO\SQLite\Driver',
    ];

    /**
     * @param key-of<self::DRIVER_MAP> $driverKey
     * @param value-of<self::DRIVER_MAP> $driverClass
     */
    public function connect(string $driverKey, string $driverClass): void
    {
        // ...
    }
}

$manager = new DriverManager();

// Valid Call
$manager->connect('pdo_mysql', 'PDO\MySQL\Driver');

// Invalid Driver Key
$manager->connect('pdo_pgsql', 'PDO\MySQL\Driver');
// Throws: TypeError: Argument $driverKey must be a key of App\Database\DriverManager::DRIVER_MAP, string 'pdo_pgsql' given

// Invalid Driver Class Value
$manager->connect('pdo_mysql', 'PDO\PgSQL\Driver');
// Throws: TypeError: Argument $driverClass must be a value of App\Database\DriverManager::DRIVER_MAP

2. Extracting from Enums (UnitEnums vs. BackedEnums)

  • UnitEnums (enum Suit { case Hearts; case Spades; }):
    • key-of<Suit> validates case names ('Hearts', 'Spades').
    • value-of<Suit> strictly rejects all values with a TypeError because pure UnitEnums possess no backing values.
  • BackedEnums (enum Status: string { case Active = 'active'; } or enum Code: int):
    • key-of<Status> validates case names ('Active').
    • value-of<Status> validates backing values ('active').
php
enum Suit
{
    case Hearts;
    case Spades;
}

enum StatusEnum: string
{
    case Active = 'active';
    case Pending = 'pending';
}

/**
 * @param key-of<Suit> $suitName          // Expects: 'Hearts' | 'Spades'
 * @param key-of<StatusEnum> $caseName    // Expects: 'Active' | 'Pending'
 * @param value-of<StatusEnum> $caseValue // Expects: 'active' | 'pending'
 */
function configureStatus(string $suitName, string $caseName, string $caseValue): void
{
    // ...
}

// 1. Valid Call
configureStatus('Hearts', 'Active', 'active');

// 2. Invalid Case Name (Passing lowercase 'active' where case name 'Active' was expected)
configureStatus('Hearts', 'active', 'active');
// Throws: TypeError: Argument $caseName must be a key of enum StatusEnum

// 3. Invalid UnitEnum value-of usage (UnitEnums have no backing values)
function testBadUnitEnumValue(mixed $val): void {}
/** @param value-of<Suit> $val */
// Throws: TypeError: Argument $val must be a value of enum Suit

3. Inline Array Shapes & Type Aliases (@phpstan-type)

key-of<T> and value-of<T> can be used directly on inline array shapes or nested inside @phpstan-type / @psalm-type aliases:

php
namespace App\Services;

use App\Database\DriverManager;

/**
 * @phpstan-type ConnectionParams array{
 *     driver: key-of<DriverManager::DRIVER_MAP>,
 *     driverClass?: value-of<DriverManager::DRIVER_MAP>
 * }
 */
class ConnectionService
{
    /**
     * @param ConnectionParams $params
     * @param key-of<array{id: int, name: string}> $shapeKey
     */
    public function configure(array $params, string $shapeKey): void
    {
        // ...
    }
}

$service = new ConnectionService();

// Valid Call
$service->configure(['driver' => 'pdo_mysql'], 'id');

// Invalid Nested Driver Key inside Type Alias
$service->configure(['driver' => 'pdo_pgsql'], 'id');
// Throws: TypeError: Argument $params['driver'] must be a key of App\Database\DriverManager::DRIVER_MAP

Offset Access Types (T[K] & T[K1][K2])

TypePHP supports evaluating offset access lookups on array shapes, constant arrays, and @phpstan-type aliases at runtime using T[K] and multi-level T[K1][K2] syntax.

AST Reduction: TypePHP evaluates and reduces offset access lookups (e.g. UserShape['id'] $\rightarrow$ positive-int) at the AST level before validation runs, executing type checks at $O(1)$ constant speed.

php
namespace App\Services;

/**
 * @phpstan-type DatabaseConfig array{
 *     connection: array{
 *         port: int<1, 65535>,
 *         driver: 'mysql'|'pgsql'
 *     }
 * }
 */
class UserService
{
    public const CONFIG_MAP = [
        'mysql' => 'PDO\MySQL\Driver',
    ];

    /**
     * Resolves Multi-Level Offset DatabaseConfig['connection']['port'] -> int<1, 65535>
     * Resolves Constant Offset self::CONFIG_MAP['mysql'] -> literal 'PDO\MySQL\Driver'
     *
     * @param DatabaseConfig['connection']['port'] $port
     * @param self::CONFIG_MAP['mysql'] $driverClass
     */
    public function configure(int $port, string $driverClass): void
    {
        // ...
    }
}

$service = new UserService();

// 1. Valid Call
$service->configure(3306, 'PDO\MySQL\Driver');

// 2. Invalid Port (70000 exceeds int<1, 65535> extracted from nested offset)
$service->configure(70000, 'PDO\MySQL\Driver');
// Throws: TypeError: Argument $port must be <= 65535, 70000 given

// 3. Invalid Driver Class ('PDO\PgSQL\Driver' violates literal 'PDO\MySQL\Driver')
$service->configure(3306, 'PDO\PgSQL\Driver');
// Throws: TypeError: Argument $driverClass must be literal 'PDO\MySQL\Driver'

Object Shapes (object{prop: type} & stdClass{prop: type})

Define property shape contracts for generic objects or strictly for stdClass instances:

Generic Object Shapes (object{prop: type})

Accepts any object instance or stdClass matching the declared property shape:

php
/**
 * @param object{id: positive-int, name: non-empty-string, role?: string} $user
 */
function processObjectShape(object $user): void
{
    // ...
}

$std = new stdClass();
$std->id = 42;
$std->name = 'Alice';

processObjectShape($std); // Valid

class CustomUser { public int $id = 42; public string $name = 'Alice'; }
processObjectShape(new CustomUser()); // Valid

Strict stdClass Shapes (stdClass{prop: type})

Strictly requires a \stdClass instance, rejecting custom class instances:

php
/**
 * @param stdClass{id: positive-int, name: non-empty-string} $payload
 */
function processStrictStdClass(object $payload): void
{
    // ...
}

// Rejects custom class instances even if they possess 'id' and 'name' properties!
class CustomUser { public int $id = 42; public string $name = 'Alice'; }

processStrictStdClass(new CustomUser());
// Throws: TypeError: processStrictStdClass(): Argument $payload must be an instance of stdClass

Reflection-Free Fast Path for stdClass

ObjectShapeValidator executes dynamic property validation on stdClass directly through native PHP property lookups, bypassing \ReflectionObject instantiation entirely to sustain throughput over 130,000 operations per second.

Safe Inspection of Uninitialized Readonly Properties

If an object instance contains uninitialized PHP 8.1+ readonly properties, TypePHP's ObjectShapeValidator safely inspects property initialization states using Reflection before attempting reads, throwing a clean TypeError: property 'id' is uninitialized without triggering PHP engine fatal crashes.