Generic types in PHP are a hotly debated topic. The reason being quite obviously that both a sizable portion of the wider PHP community and of the internals developers want generics. As I will explain below, I do as well. This new RFC [1] is well put together and comprehensive, however, it makes a fundamental design choice that means it is a non-starter for myself, and many other people, it being the fact that the generic types are erased.
The appeal for erased generic types is understandable: you get native syntax, some basic checks but the hard part of figuring out how to attach type information to an object instance rather than on the class is left out. However, for those of us that maintain and design the language we are required to think about PHP in its entirety and have it be coherent and consistent for every user of the language, not just (as I will explain) a minority of users.
PHP has, since version 5.0.0 [2], had runtime type checking. This predates the strict_types declare statement by over a decade. The fact that PHP allows strings, integers, floats, and booleans to type coerce between themselves when strict_types is disabled doesn’t change this fundamental fact about PHP.
Introducing native types that are not type checked to PHP would create a massive inconsistency and incoherence. This is confirmed by my discussions in person and online in the last few weeks, most people assume that this native syntax gives them natively type checked generic types, and when told this is not the case respond with confusion and apprehension. The fact that the distinction is easy to explain doesn’t counteract the immensely counterintuitive behaviour, as some of the easiest software patterns to explain, such as using prepared statements or not storing passwords in plain text, continue to be prevalent. Therefore, I believe this argument is extremely weak and any attempt to hand-wave this issue away will always be met with suspicion by internals.
Proponents of erased generic types claim that only users of Static Analysis (SA) tools would use this feature. Moreover, proponents [seem to?] believe, and routinely assert to me, that “90% of projects” use SA tools. This feels like an XKCD 2501 (Average Familiarity) situation, [3] as this statistic is quite literally an unsupported fantasy and not representative of the reality that I, and many other consultants I’ve talked to, live in.
The best data we have to verify the veracity of this statement comes from the JetBrains “Developer Ecosystem Survey 2025” [4] which found that 36% of respondents use PHPStan and 8% use Psalm. [4, Code quality tools] Now, assuming this survey had a representative sample of PHP users (which is debatable, as we can assert that people responding to surveys are on average more engaged than the run of the mill developer), and that nobody uses both PHPStan and Psalm at the same time (which isn’t true as many libraries use both if they use Psalm), we would get at best 44% of users employing SA tools. A far cry from the 90% number I’m constantly told.
Considering this same survey showed that 40% of people currently use no code quality tools [4, Code quality tools], and that 30% of respondents do not write any tests at all [4, Debugging and testing] (a number that has been consistent over the last 6 years) I doubt we will ever reach the wide adoption of SA tools that proponents of this RFC claim already exists.
If this feature lands in PHP it is a statistical inevitability that it will be utilised by users without using a SA tool at the same time. Especially when libraries start using this feature, as consumers of those libraries would not be forced to use a SA tool to validate their code. Nor is it possible for us to mandate the use of SA tools provided by the community, some of them commercial, which also reject perfectly valid PHP code (e.g. not initialising all properties in the constructor) for a variety of reasons, or are unable to reason about relatively simple meta-programming code. Therefore, PHP would need to provide an official SA tool which implements those checks without the opinionated rules of community based tools.
Let’s contrast this to Java (and other JVM based languages like Kotlin and Scala) or TypeScript, the only programming languages that have erased generics, where users are forced to have a “build”/compile step that validates the types ahead of time, something that would not be the case in PHP according to this proposal. You might shout “hold on a minute Python also has erased types”, and this is true, but contrary to PHP, all types in Python are erased and no runtime type checking is performed at all. This is consistent in contrast to to this proposal which would have PHP do runtime type checking most of the time, but not always.
Interestingly, in the “2025 State of JS” survey [5], 28% of respondents complained about the lack of static typing in JavaScript. [5, Missing Features] However, when asked about how static typing should be implemented, 49% of respondents answered that it should be implemented as type annotations, while 32% of respondents hope for runtime types that are checked. [5, Native Types] Even in an ecosystem used to erased types, a third of users would prefer runtime checked types. This puts the notion that erased types are desirable to the test.
PHP already has a syntax for denoting types which are ignored by the interpreter at runtime and which are not type checked: they are called doc comments. Now, one of the main arguments in favour of this RFC is that it is “nicer” to use a native syntax than doc comments. While this may be true, the addition of a native generic syntax would not remove the need to write doc comments for static analysis tools.
Indeed, the majority of static analysis tools introduced additional atomic types that do not exist within PHP, some examples are: [6], [7]
class-stringnon-empty-listpositive-intnumericint<0, 100>If the main purpose of this proposal is to allow users to write types in a “native-esque” syntax, I feel it would be wiser to write a “small” transpiler that converts:
class Foo<T> {
public function bar(T $t): int<0, 100> {}
}
into
/** @template T */
class Foo {
/**
* @param T $item
* @return int<0, 100>
*/
public function bar($t): int {}
}
While no transpiler that I know of does exactly the above, other people have written transpilers for PHP:
async/await syntax into calls of the AMP async frameworkGiven how many similar tools already exist, writing such a transpiler is not a far fetched idea. However, there are multiple counter-arguments thrown back at me every time I mention this.
First, the lack of support from Composer, something that is contradicted by the existence of Phabel. Regardless, this seems like a trivial matter to me: suppose a library uses such a transpiler, then its release process would include this “build” step, the published library would be the PHP code artefacts themselves, and end-users would not have to interact with the transpiler step.
Secondly, because PHP has no build step, users and the ecosystem just expect to be able to run a PHP script using the php command. But if one uses an SA tool, which this feature is targeted at, one is already engaging in a sort of build step. Thus bolting on a transpiler that combines the preprocessing and static analysis steps would, IMO, be a very similar experience for users of SA tools running the tool prior to execution.
The final argument is that none of these tools have reached wide adoption, and such near-universal adoption would only be realistic with the backing of a massive company, such as Microsoft with TypeScript. This ignores some facts which could also explain the limited adoption. The lack of promotion of these tools, especially by erased generics advocates, means that people are unaware these tools even exist. But more importantly, because the use of SA tools is not that widespread, the need for a transpiler is even less widespread.
I want to mention Java again, as it should be noted that the only reason it doesn’t reify all of its types is due to backward compatibility concerns. [13] This is an important point to touch on, as the proposed path to enable runtime type checking by the RFC is to make it opt-in. [1, Opt-in reified generics] Which is in my view a completely nonsensical proposal. Why would one need more native syntax for it to do what one would expect from the beginning? But the explanation is simple, if we would go from erased to type checked we would break compatibility with code using generic types, as one could write and execute code in production that violates the erased generic type. This situation can only occur due to the lack of a mandatory ahead-of-time static type check of the code-base.
A final argument in favour of erased generic types is a concern about performance. Type checking at runtime is inherently slower than not doing so, especially as type checking compound generic types follows a super-linear time complexity (O(n log(n))). However compound generic types are rare in practice, indeed I asked Damien Seguy to do some analysis on roughly 3200 open source PHP projects, and he found around 250 cases of compound generic types. (And a bunch of them were with the array type, which is usually excluded when talking about generic types.) This is peanuts, and not something we should be worried about.
Moreover, PHP is a fast interpreted programming language challenged only by JavaScript’s V8 engine [14], thus trading away the developer experience benefits of runtime-checked types for erased generic types does not seem worth it to me. Especially considering the fact there are still things we could do, be that in the optimiser or the compiler, to eliminate certain type checks. One such case is a PR I’m working on to drop return type checks when returning $this. [15]
Gradual and incremental implementation of runtime checked generic types in PHP. This is not just a mirage, as I have a working prototype [16], which I’ve been working on and off for the past year, for generic types restricted to interfaces with runtime checked generic types.
Does this implementation provide full-blown generics with all the bells and whistles? Definitely not. It only supports invariant generic types, and one can only define the generic types on an interface. However, I think it’s worth remembering the evolution of PHP’s native types:
array type [17]callable type [18]int, string, bool, float) types, [19] and support for return types [20]iterable [21], void [22], and nullable types [23]object type [24] and limited contravariance for parameters [25]mixed [28], static [29], and union types [30]never [31], and pure intersection types [32]true [33], false, null singleton types [34], and DNF types [35]As one can see, PHP’s type system has evolved gradually and incrementally over the course of its existence. Why should generics follow a different path? It is simpler to introduce such a large feature in chunks, and figure out the details one step at a time.
The usual counterargument is that such an implementation doesn’t guarantee that we end up with “full generics” in the future, and that somehow anything less than that is useless and devoid of value. I personally disagree with this, as even just the ability to use generic types on an internal interface would be useful. For example the ArrayAccess interface currently forces implementing classes to use the mixed type for parameters:
class AnimalList implements ArrayAccess {
public function offsetExists(mixed $offset): bool { /* ... */ }
public function offsetGet(mixed $offset): mixed { /* ... */ }
public function offsetSet(mixed $offset, mixed $value): void { /* ... */ }
public function offsetUnset(mixed $offset): void { /* ... */ }
}
This means that one can pass values of the wrong type as the offset or value to this class, which either requires manual type checking or trusting the user of the class. Instead, if the engine could define the interface as ArrayAccess<TOffset, TValue> one could write:
class AnimalList implements ArrayAccess<int, Animal> {
public function offsetExists(int $offset): bool { /* ... */ }
public function offsetGet(int $offset): Animal { /* ... */ }
public function offsetSet(int $offset, Animal $value): void { /* ... */ }
public function offsetUnset(int $offset): void { /* ... */ }
}
which would restore the code to the usual runtime type safety that PHP has. The proposed RFC [1] does permit this, but without the ability to have a function verify at runtime that a parameter typed with ArrayAccess<int, Animal> is actually given such a value.
I also think splitting off the already hard task of implementing (co and contra-variant) generics from the even harder part of determining how to “attach” type information to the instance of an object rather than its class is somewhat sensible.
Hopefully I’ve explained why myself and many other internal and community people are against any erased typing feature.
It is not that we don’t see or understand the appeal or usefulness of erased generics, but that messing up the fundamental semantics of the language does not, and never will, have more benefits than drawbacks.
PHP has recently engaged in tightening its semantics to be more coherent and consistent, and stepping back from this positive trend would be quite a shame.