PHP Erased Generics RFC

moebrowne1 pts1 comments

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...

type function value generics public bound

Related Articles