Arrays and lists
PHP has one runtime collection type — the array — but the type system distinguishes two views of it: keyed arrays (the general form, with key and value parameters) and lists (int-keyed, contiguous, with a single element type). Both views support generic and shape forms. Shapes can be sealed or unsealed.
| PHP-side | Denotes |
|---|---|
array<K, V>, array{a: int, b?: string, ...<K, V>}, array{} | Keyed array. |
list<T>, non-empty-list<T>, list{0: int, 1: string, ...<T>} | Int-keyed contiguous list. |
This chapter describes what each means. The lattice rules for subtyping, meet, join, and so on are in refines, meet, and join.
Keyed arrays
A keyed array can be in one of four states:
| State | PHP-side | Notes |
|---|---|---|
| Generic | array<K, V>, non-empty-array<K, V> | K and V are type parameters. Any entries allowed, all keys constrained by K, all values by V. |
| Unsealed shape | array{a: int, ...<K, V>} | Specific known entries plus a rest type for unknown entries. The bare ... is shorthand for ...<array-key, mixed>. |
| Sealed shape | array{a: int} | Specific entries; no extras. |
| Empty | array{} | The single empty array. |
The unsealed shape's rest parameters constrain the additional entries beyond the listed ones ; they say nothing about the listed entries' types. So array{a: int, ...<string, bool>} means: the entry a is an int, and any other entries have string keys and bool values. The bare ... form is the common case where the user does not want to constrain the extras and is exactly equivalent to ...<array-key, mixed>.
Known entries
A shape's entries each carry:
- A key (an integer literal like
0, a string literal like"name", or a class constant likeFoo::KEY). - A value type.
- An
optionalflag (PHP?:array{name?: string}).
The lattice's refines chapter has the full required-vs-optional rules.
Sealed vs unsealed
A sealed shape commits to having no entries beyond those listed. An unsealed shape allows additional entries, with their key and value types constrained by the rest parameters (...<K, V>, defaulting to ...<array-key, mixed>). The lattice consequences:
array{a: int, b: string}(sealed) refinesarray{a: int}(sealed) only if the input has exactly the keys the container does (extras are forbidden).array{a: int, b: string}(sealed) refinesarray{a: int, ...}(unsealed) by ignoring the extras.array{a: int, b: string}refinesarray{a: int, ...<string, bool>}only if every extra entry beyondahas astringkey and aboolvalue ; hereb: stringhas a string key but astringvalue, so it fails the rest constraint.array{}refines every keyed-array container that admits empty (i.e. is notnon-empty-array).
non-empty-array
The non-empty axis asserts that the array has at least one entry. A shape with at least one required entry is automatically non-empty; the explicit form exists for the generic case.
Lists
A list is structurally an int-keyed array with contiguous keys starting at 0. Suffete carries it as a separate family because PHP analysers commonly distinguish them and many operations are sharper in list-shape (the keys are positional, not arbitrary).
| State | PHP-side | Notes |
|---|---|---|
| Generic | list<T>, non-empty-list<T> | Single element type. Any number of entries allowed, all values constrained by T. |
| Unsealed shape | list{0: int, ...<T>} | Known prefix plus a rest element type. The bare list{0: int, ...} is shorthand for ...<mixed>. |
| Sealed shape | list{0: int, 1: string} | Fixed-shape list, no extras. |
| Empty | list{} | The single empty list (equivalent to array{} viewed as a list). |
Known elements
Indices start at 0; required entries must be contiguous from 0. Optional entries can be skipped only at the tail (PHP supports [int, int, ?string] but not [int, ?int, int]).
Cross-family relationships
list<int>refinesarray<int, int>; lists are int-keyed arrays.non-empty-list<int>refineslist<int>; non-empty is a strict refinement.list{0: int, 1: string}(sealed) refineslist<int|string>; sealed-vs-generic with element-type covering all known types.- An empty sealed list (
list{}) is uninhabited if combined withnon-empty.
How keyed arrays and lists relate
graph BT
SealList["list{0: int, 1: int}"] --> NonEmptyList["non-empty-list<int>"]
NonEmptyList --> List["list<int>"]
List --> KArray["array<int, int>"]
SealedKArray["array{0: int, 1: int}"] --> KArray
KArray --> Empty["array<mixed, mixed>"]
A list refines a keyed-array of compatible parameters when the container's key parameter accepts int and the value parameter accepts the list's element type.
A worked example
/**
* @param array{name: string, age: int} $u
*/
function f(array $u): void { /* ... */ }
f(['name' => 'Hannibal', 'age' => 24]); // OK: matches the shape
f(['name' => 'Hannibal']); // FAIL: missing required 'age'
f(['name' => 'Hannibal', 'age' => 24, 'rank' => 'general']); // FAIL: extra key, sealed
The parameter is a sealed keyed-array shape; the argument types are inferred (or user-annotated) sealed shapes. The sealed-vs-sealed rule fires and decides each case.
Intersections
array<K, V> & SomeConjunct and list<T> & SomeConjunct are expressed via the Intersected wrapper.
See also: refines for the per-pair subtype rules; iterables and callables for how arrays relate to
iterable<K, V>; meet for sealed-shape intersection.