Over the last year, I’ve been writing more and more real production code with AI. Not just small snippets, but the sort of work that usually fills half a sprint. Things like migrations, content transforms, scaffolding and all the glue that sits between the interesting parts of a project.
And during all of that, something kept happening that I couldn’t ignore. Whenever I asked an AI to write TypeScript, the output felt calm and predictable. When I asked it to write PHP, it was noticeably less consistent. Same model. Same task. Very different outcomes.
At first, I assumed it was something I was doing. Maybe I was prompting differently or rushing. But the pattern kept showing up across larger work, especially during a client modernisation project. PHP is where I spend most of my time, so the inconsistency stood out.
Eventually, I had to ask the obvious question: why does AI seem so much more comfortable in TypeScript than in PHP?
The answer turned out to be much simpler than I expected. AI performs better when the code gives it clear, explicit clues. TypeScript provides the model with those clues. PHP, most of the time, doesn’t. But the good news is that we can give PHP those same advantages today with the tools we already have.
AI feels more reliable in TypeScript for a reason.
If you’ve used Copilot or any other AI coding tool inside your editor, you’ve probably seen this difference yourself. When you’re in plain JavaScript, the model has a habit of drifting a little. It tries things out, it makes assumptions, and sometimes it gets it right, but plenty of times it wanders off somewhere you didn’t expect.
Move the same task into TypeScript, and everything feels calmer. The suggestions are more grounded. The model hits the right shapes, the correct parameters, the right return types, and you barely have to think about whether the output will make sense.
The reason for that isn’t mysterious at all. TypeScript gives the model a clear picture of the world it’s working in. The shapes are well defined, the expectations are explicit, and there’s very little room for the model to fill in the blanks with guesswork.
Once that clicked for me, the PHP problem suddenly looked a lot less confusing. PHP wasn’t the issue. The lack of structure was.
A quick look at ambiguity
Before getting into the PHP and WordPress side of things, it helps to take a quick look at how ambiguity shows up in code. You’ve probably seen this play out if you’ve used any AI coding tool for more than a few minutes.
If you take something as simple as a JavaScript function like:
function process(data) {
// ...
}Code language: JavaScript (javascript)
There’s nothing here for the AI to work with. The model has no idea what data is supposed to be. It might be a string, a number, an object, an array, or something completely different. So the model does what it always does when the world is fuzzy. It guesses. And depending on the size of the guess, the output might drift quite a long way from what you expected.
Now contrast that with a TypeScript version:
function process(data: UserPost) {
// ...
}Code language: TypeScript (typescript)
Suddenly, everything is much clearer. The model knows exactly what data should look like. It knows the shape, it knows the fields, and it knows how this thing is meant to behave. As soon as the model has that information, the suggestions become more confident and more accurate. It’s not fighting the unknown anymore.
That’s really the heart of this whole discussion. Strong types reduce uncertainty. And uncertainty is what causes AI to drift, guess, or overcompensate. When you move from TypeScript to PHP, you’re not losing features. You’re losing clarity. The language stops telling the model what to expect, and the model slips straight back into guesswork.
What the research says
This isn’t just something I’ve noticed in my own work. There’s quite a bit of research now showing the same thing. Typed languages consistently produce better AI-generated code.
For example, one study on type-constrained generation in TypeScript found that when the type system guided the model, compilation errors dropped by more than half and functional correctness improved across a range of models.
Another paper, from 2024, explored how providing the model with explicit type and binding information improves code completion. The whole idea is that the more structured the environment, the fewer opportunities the model has to drift.
Put all this together, and the message is clear. Strong types reduce uncertainty. Less uncertainty means better output, regardless of the model you’re using.
Why PHP and WordPress make life harder for AI
PHP is dynamic by default, and WordPress is dynamic by design, so when you put the two together, you end up with a lot of ambiguity. Humans handle that instinctively because we’ve all lived in the WordPress ecosystem long enough to know what to expect. We know when a function might return an object, an integer, or false. We see the shape of specific fields, even if nobody’s written it down. We know which arrays always contain a particular key, even though the documentation says nothing about it. And we know when something is essentially always a string, even if the function signature claims it could be anything.
But AI doesn’t know any of that. It doesn’t have that instinctive model of how WordPress behaves, and it definitely can’t lean on “experience” the way a developer can. So it guesses. And that’s when you start seeing those odd behaviours crop up. Things like passing a post ID into a function that expects a WP_Post, or accessing array keys that never existed, or mixing up associative arrays and lists, or returning shapes that the next bit of code quietly depends on.
All of this matches exactly what the research says about dynamic languages. When a language or framework leaves a lot of things unsaid, the model has no choice but to fill in the blanks. And guessing is where things go wrong.
Even when the AI gets it right, the code becomes bloated
This is something you notice straight away when reading AI-generated PHP.
Because the model isn’t sure what it’s dealing with, it tends to swing to one extreme or the other. Sometimes it becomes incredibly defensive. It checks everything, casts everything, and wraps every line in a little safety blanket, as if the whole function might explode at any moment. It tries to account for every possible timeline, even when the real code path is much simpler.
But other times it does the complete opposite and charges in with far too much confidence. It assumes a post ID is definitely a WP_Post object, or that an array definitely has a particular key, and it just carries on without verifying either. That sort of bravado reads fine in the AI’s output, but it obviously falls apart the moment you try to run the code.
Both behaviours come from the same place. The model doesn’t actually know what shape the data has, so it’s forced to either overprotect itself or bluff its way through. And neither approach gives you the clean, reliable code you want.
That’s how we end up with code like this:
/**
* Build a post teaser array.
*
* @return array
*/
function build_post_teaser( $post ) {
if ( is_int( $post ) ) {
$post = get_post( $post );
}
if ( ! $post instanceof \WP_Post ) {
return [];
}
$url = get_permalink( $post ) ?: '';
return [
'id' => $post->ID,
'title' => $post->post_title,
'url' => $url,
];
}Code language: PHP (php)
None of this is technically wrong, but it’s noisy. The intent gets lost in the safety checks.
Once you give the model clearer expectations, the whole thing calms down:
/**
* Build a post teaser array.
*
* @return array{id: int, title: string, url: string}
*/
function build_post_teaser( WP_Post $post ): array {
return [
'id' => $post->ID,
'title' => $post->post_title,
'url' => get_permalink( $post ),
];
}Code language: PHP (php)
Teaser arrays are one thing. But the drift gets much worse when the ambiguity compounds inside WordPress’s larger APIs. WP_Query is a perfect example. Here’s what AI often produces when it doesn’t know what shape $query->posts contains
/**
* Get an array of articles for the homepage.
*
* @return array
*/
function build_homepage_articles() {
$query = new WP_Query( [
'post_type' => 'article',
'posts_per_page' => 10,
] );
$articles = [];
foreach ( $query->posts as $post ) {
$articles[] = [
'id' => $post['ID'] ?? 0,
'title' => $post['title'] ?? '',
'url' => get_permalink( $post['ID'] ?? null ),
];
}
return $articles;
}Code language: PHP (php)
Once you spell out the structure, the whole function settles down
/**
* Get an array of articles for the homepage.
*
* @return list<array{id: int, title: string, url: string}>
*/
function build_articles(): array {
$query = new WP_Query( [
'post_type' => 'article',
'posts_per_page' => 10,
] );
/** @var list<WP_Post> $posts */
$posts = $query->posts;
return array_map(
fn( WP_Post $post ) => [
'id' => $post->ID,
'title' => $post->post_title,
'url' => get_permalink( $post ),
],
$posts
);
}Code language: PHP (php)
When you add those clearer expectations, the code becomes shorter and easier to read, and it feels a lot more predictable. You’re no longer wading through layers of defensive checks. You can actually see the function’s intent again, which makes it easier to work with for both humans and AI. And none of this is magic. It’s simply the result of the model finally knowing what you expect from it.
You don’t need to wrap WordPress. You just need to be clear.
One of the biggest things I realised while digging into all of this is that we don’t need to wrap WordPress to make AI behave. In fact, trying to wrap every WordPress function usually makes things worse. You end up adding layers nobody asked for, and pretty soon, the team is maintaining abstractions instead of just building the feature they set out to build in the first place.
The real issue isn’t WordPress itself. It’s the ambiguity that creeps in with how we use it. WordPress will always return different types depending on what you give it. Sometimes you’ll get integers, sometimes objects, sometimes an array with a shape nobody has written down since 2012. That’s just how WordPress works, and that’s fine.
What matters is how clearly we express our intent.
If a query is going to return integers because you’ve set fields => 'ids', then just say so:
/** @var list<int> $ids */
$ids = get_posts(
[
'fields' => 'ids',
'post_type' => 'article',
]
);Code language: PHP (php)
Now AI knows what you expect, and so does the next person who opens the file six months from now. And once you start doing this consistently, shaping your arrays, using PHPDoc to fill in the gaps WordPress leaves open, being clear about your function inputs and outputs, your PHP starts to feel much more like TypeScript from AI’s point of view. You haven’t changed the language at all. You’ve just removed the uncertainty that forced the model to guess in the first place.
Tools that help bring structure to PHP
Strict types make a difference straight away. When you add declare( strict_types = 1 ) to new files, you cut out a lot of silent ambiguity that PHP would usually let slide. It’s a small change, but it sets a clear expectation for both humans and AI.
PHPStan is another big part of this. On a client project, for example, we run it at level 10 for the migration work and level 8 for general development. That gives us a really solid safety net. It catches the sort of mistakes PHP would happily let through, and it pushes you to be more transparent about your intent.
And then there’s PHPDoc, which fills the gaps in places where PHP’s type system can’t. You can describe generics, shapes, lists, dictionaries, whatever you need. It might feel like you’re just adding comments, but AI systems read those comments exactly the same way a human does. They give the model enough context to stay on the right path instead of wandering into guesswork.
What this looks like in practice
On a client project, we leaned pretty heavily on agent-based workflows for content transforms and migrations. These transforms involved many shape changes. Classic Editor content had to be converted to blocks. Old arrays had to be converted to structured data. It was detailed, repetitive work with a lot of moving parts.
What surprised me was how good AI became at all the glue once we gave it something solid to stand on. The moment our transform functions had clear, typed inputs and outputs, shaped arrays, proper PHPDoc generics, and rules enforced by PHPStan, everything changed. The model stopped wandering. It stopped producing defensive boilerplate. It stopped making odd little mistakes that we’d have to clean up later.
Instead, the transforms became calmer and easier to review. The intent was obvious. The code felt consistent. And patterns that used to be noisy or ambiguous became simple to work with again.
It was such a clear pattern. The more structure we added, the more predictable and reliable AI became.
When you scale this across dozens of transforms per sprint, the difference becomes clearer. Before we added structure, we spent a lot of time untangling small mistakes: a missing key here, a mis-typed value there, a post ID treated like an object. Individually, they were minor problems, but together they created friction and slowed reviews. Once we introduced stricter shapes and clearer intent, the volume of these issues dropped so sharply that the whole workflow felt lighter. The team could focus on the parts that actually required human judgment rather than cleaning up minor inconsistencies.
How to bring this into your own codebase
You don’t need to overhaul everything at once. In fact, the best results come from starting small and letting the structure build up over time. Begin by adding strict types to any new files you work on. Shape your arrays so it’s clear what they contain. Use PHPDoc to fill in the gaps where WordPress leaves things vague or open-ended. And bring PHPStan into the picture, even if you start at a lower level and gradually move up.
If you’re working in a large or older codebase, using a PHPStan baseline is a lifesaver. It lets you say “we want to start at level 8” without being blocked by all the existing issues in the codebase. You freeze the current problems, then fix them naturally over time as you touch different parts of the system. Meanwhile, all new code has to meet the higher standard. It’s a really effective way to raise the overall quality without stopping everyone’s work for a giant refactor.
These small steps add up quickly. Once you introduce a bit more clarity and shape to the code, you’ll feel the difference almost immediately, both in what you write yourself and in what AI produces for you.
Final thoughts
The idea behind all of this is simple. AI needs structure. Types give it that structure. And once you start writing PHP with the same clarity you expect from TypeScript, everything gets easier. The code gets quieter. Reviews get faster. The AI stops wandering. And you can spend far more time on the work that actually matters, rather than cleaning up the parts that never should have gone wrong in the first place.
This isn’t about perfection or purity. It’s about making your life easier. When your code has clearer intent, both humans and machines benefit from it. And once you feel the difference, it’s hard to go back.