PHP: rfc:bound_erased_generic_types
rfc:bound_erased_generic_types
PHP RFC: Bound-Erased Generic Types
Version: 0.22
Date: 2026-05-08
Author: Seifeddine Gmati, azjezz@carthage.software
Status: Under discussion
Implementation: https://github.com/php/php-src/pull/21969
Discussion thread: https://news-web.php.net/php.internals/130816
Introduction
This RFC adds generic type syntax to PHP. Classes, interfaces, traits,<br>functions, methods, closures, and arrow functions can declare type<br>parameters; those parameters carry bounds, defaults, and variance markers;<br>type arguments may be supplied at use sites and at call sites via turbofish.
final readonly class PairL, +R> {<br>public function __construct(<br>public L $left,<br>public R $right,<br>) {}
public function swap(): PairR, L> {<br>return new Pair($this->right, $this->left);
final readonly class BoxT> {<br>public function __construct(<br>public T $value,<br>) {}
public function mapU>(callable $fn): BoxU> {<br>return new Box(($fn)($this->value));
public function zipO>(O $value): BoxPairT, O>> {<br>return new Box(new Pair($this->value, $value));
function identityT>(T $value): T {<br>return $value;
$greeting = new Box::string>("hello, world");<br>$paired = $greeting->zip::int>(42);<br>$swapped = $paired->value->swap();<br>$result = identity::Pairint, string>>($swapped);
var_dump($result->left); // int(42)<br>var_dump($result->right); // string(12) "hello, world"
Generics in this proposal are bound-erased: at runtime, Box<br>and Box are the same class, each type parameter is replaced by<br>its declared bound (or mixed when unbounded), and the engine sees<br>ordinary PHP types. Enforcement is split across three layers:
Compile time validates syntax, the 127-argument cap, and that a type parameter's default satisfies its declared bound.
Link time validates inheritance-clause arity (extends/implements/use), bound conformance with bound-on-bound for forwarded parameters, rejection of diamond inheritance with conflicting bindings, and parametric LSP propagated into property types, property hook signatures, trait method signatures, and non-overridden inherited method signatures.
Runtime validates arity and bounds at every turbofish site - calls, new, and attribute construction. Backed property storage, property hook signatures, and trait properties enforce the substituted type.
Together these three sites enforce every invariant this RFC makes about<br>generic code, with a small enumerated set of corner cases (documented in<br>Limitations) where the runtime is intentionally laxer<br>than the substituted signature would suggest. The design is “mostly<br>enforced at runtime, fully enforced at the static-analysis layer the<br>ecosystem already runs”. The corner cases are bounded, listed, and<br>reachable only by code that the existing analyzers would already flag.
Type arguments at call sites are written :: (turbofish) and are<br>optional everywhere they appear . Adding generic parameters to a<br>function does not require any caller to change: existing call sites<br>continue to type-check and run unchanged, and turbofish may be added<br>later at the call sites that want it (for static-analysis disambiguation<br>or for the runtime arity/bounds check it triggers). This is the BC-<br>smoothing layer that lets generic types be adopted incrementally,<br>without coordinated breakage of the consumers of a library. As a<br>concrete example, a function that today is written:
function pick(mixed $left, mixed $right): mixed<br>return rand(0, 1) ? $left : $right;
can be migrated to:
function pickL, R>(L $left, R $right): L|R<br>return rand(0, 1) ? $left : $right;
The migration sharpens the static type, removes the mixed parameter<br>holes, and gives analyzers a precise return type for every call -<br>without changing the signature shape the runtime sees, and without<br>requiring any caller to write pick::($a, $b). The<br>existing pick($a, $b) call continues to compile and run identically.
A new Reflection API exposes the pre-erasure form, so static-analysis tools<br>read generic information directly from the engine instead of from docblock<br>conventions. Code that does not use generics at all compiles to the same<br>bytecode it does today.
The rest of this RFC walks through what generics are, why PHP needs them,<br>the prior art that shaped this design, the full proposal, the design<br>rationale, the implementation, and what runtime models can layer on top<br>later.
What are generics
Generics are to types what functions are to values.
A function abstracts over a value: sum($a, $b) computes a result from two<br>inputs, and the same function body applies whether $a and $b are 1<br>and 2 or 100 and 200. Generics abstract over a type: a generic<br>Stack holds elements of some type T, and the same class body<br>applies whether T is int, string, or User.
Without generics, two unattractive options remain:
Write the same data structure once per element type (IntStack, StringStack, UserStack). The implementations are near-identical except for the type names; bug fixes have to be made in every...