3 Rules for De-Slopping AI-Generated TypeScript Code
Hi, my name is Mike and I am an AI aholic. Hi Mike. I have a confession to make. My last big feature that I worked on, [music] you know, the one the AI files one, the one that I just did a whole big video on, well, it's total slop. You see, I just I just got so carried away with the speed of development the AI gave me that I just didn't stop just stopped to think of the consequences. Okay, enough of that. So basically the problem was that I didn't actually truly understand what the code was doing. Now don't get me wrong, I understood the feature from like a highle perspective, but when I was getting requests about specific parts of the code, I was kind of like, and that's not great when an important feature like this is on the critical path for our users. So I decided, no, enough was enough. I'm going to get my hands dirty and wade through this slop to try and uncover exactly what's going on here. I learned some valuable lessons along the way on my D-soping journey that I think are worth sharing. So stick around as I have three actionable things that you can do to dlop your code today and I might even share a skill at the end that can help you because you know the only way to fix AI slop is with more AI, right? Anyways, once you grab yourself a lovely cup of tea, drop me a like and sub. We'll get into it. Let's just start off with a quick reminder of this feature if you haven't watched my previous videos on this already. But basically, AI files is going to inject this additional prompt when you set up a convex project asking you whether you would like to add the AI files to your project. Now, AI files are guidelines and agent skills that are designed to enhance the model performance when writing convex code. And as part of this feature, there's a new CLI that you can use to update the AI files or remove them or a few other things. Okay. So, so far so good. Seems pretty simple, right? Yeah. Well, the issue is that there are a number of different logical flows that the code can go through. Whether you're running convex dev for the first time or subsequent times or are you running from the AI files CLI or you running for the convex dev path, are you a human or are you an agent or are you on the CI or some other sort of non-interactive prompt? And so in short, we just have this tangle of logic and rules that's just hard to follow at the better times, let alone when the code looks something like this. Yeah. So most of the code here is contained in this 1,000line index file uh that I personally find really hard to know where even to start when code looks like this. So my first step is to break this up into smaller files, areas of different responsibility. So now I think it's much better. We have a number of smaller files that pertain to key areas such as the agents MD, Claude's MD, guideline skills, etc. By the way, as we go through this, I just want to note that I didn't actually do all this manually by hand. I'm not some kind of savage. I just swap to a smaller model, smaller, faster model, and then I would just queue up a bunch of orders like split this off here, split that bit off there, etc. Now maybe I could have given the AI a highle goal such as split this up into you know sensible uh sections and it would have worked fine but for me part of the reason for doing this d-sopping process doing this first initial dlopping process is for me to get a better grasp of where the various parts of the codebase are and what the various functions are. So this little bit of extra manual work [music] is acceptable for me at this stage. Okay. Okay, so now that we've split things up a bit and I have at least a general highlevel idea of the different parts of the codebase, I began working on the actual code itself. So, as an example, let's take a look at one of these gnarly tangled functions that I've been talking about. Yeah. So, if this makes you go a little bit crosseyed, you aren't alone, as that's exactly what I thought when I first saw it. Not only is it far too long, it's al also just doing like far too many separate things. It's kind of hard to tell where one part ends and another one begins or when one part begins and another part ends. You know, there's also a bunch of this nesting and subnesting logic in here and error handling which makes me just go. Now, when I encounter a big gnarly function like this, the first thing I try to understand is what is actually the purpose of this function? What is it trying to do? So, we actually have a comment at the top here, which is handy. But don't forget, you can't always trust comments because they can very easily go stale. But what it does say is, write slashupdate all convex AI files, guidelines, agents, MD, claude, MD, skills, etc. Yeah. So, a quick scan uh of the code here makes me think this comment probably isn't stale, which is nice. I'm seeing parts that seem to be handling the various um different functions as mentioned. So this most sensible thing that I would do next here is to split this into different sub functions and that results in something that looks more like this. Ah that's much better in my opinion. Now we have a clear set of things that the function is going to undertake extracting out each part to its own little self-contained function. It's also much easier to understand what this function is just going to do at a glance and allows us to easily make more changes in the future. Now, I don't want to bore you guys and going through every minor clean code practice that I followed here. I have a couple of videos on this topic already. Uh if you want to know more, but I do want to just quickly dive into this function read AI config or default as it illustrates a couple of things that I'm aiming for. Note how we named this function read AI state or default. So it's clear from the name of the function what it's going to do. And also note that it's going to call into this attempt read AI state function right off the bat. Note that we named it read AI state or default. So it's super clear from the name of the function what it's supposed to be doing. And you can see that it handles the various different code paths that could potentially come out of this function, the [music] attempt read AI state, which is actually doing the reading. And if we just double click into there, you'll note that this function again is is being prefixed with a sensible name attempt here. So it indicates that it could potentially fail, but we are also going to be catching those errors [music] and leaving it up to you to deal with those errors. So this clear separation of responsibilities between the function and the caller of that function makes the responsibilities the different layers of responsibilities on the call stack very clear and in my opinion critical to making code more readable and less sloppy. Okay, now let's get to Okay, so if you're like me and are facing a big ball of slop, here's some key actionable things that you can undertake to untangle it all. One, break large functions down into small, more manageable functions. Now, we've already talked about this a little bit, but it's just a really easy thing you can do to improve the code clarity. Just select a chunk of code and tell the model to stick this into its own self-contained [music] function. Make sure you tell the model explicitly or through agents MD to give it a nice descriptive name so that the code becomes self-documenting. Two, segregate query from the command. So what I mean by that is the AI well and junior coders in general probably have a habit of colloccating the query with the command. To show you what I mean, check out this example from this function before the refractor. So we have this function check AI files staleness. It sounds like it's going to check to see if the AI files are stale. But as we can see from the code and even from the comments up the top here, it's also going to perform the action or the command if basically which is going to log out the message if the AI files are indeed stale. So really this function should be called check AI file staleness and log out if stale. But really we could also make it even more clear by splitting out the [music] actual checking from performing the actual command which is the logging. So now we hand off this actual checking to this determine AI file staleness function. It returns a union of literals and then we just handle each one performing whatever action is required. And then right at the end here, we drop an exhaustive check to make sure that we've handled every branch and then future branches are going to get picked up by the TypeScript compiler. In my opinion, this is far more clear and easy to reason about and know where to make changes in the future. Three, name things correctly. The function should be named based upon what it should be doing. As we saw in the last example, it's not simply checking the AI file steness. It's also logging the output. So you should name it as such. Named functions serve as instant documentation to the caller of your function and future readers of your code. So be them humans or AI. Okay, now I know what you're thinking because I'm thinking it too. Can't we just get AI to solve our AI created slot problems? Well, you'd be in luck because the answer is yes, as I have attempted to distill all of this knowledge down [music] into this skill. So, if you want a just do it like Mike would do it, then run this D slop skill. I've tested it quite a few times and put it through a batcher of evals and it does a pretty good job. If you do try it out though, please do leave me a comment down below cuz I'm very interested to hear how you get on with it. And if you find issues with it that could be improved, then don't hesitate to open a PR or an issue. Stay tuned, though, as I might be adding more skills there in the future, too. But for now, I think I'm just going to leave it here as I have more slop to write. But if you did like this video, then I strongly suggest you go back and watch my other two clean code videos that I did back last year. Here's a link to one of them right here. But that's just about it for me for today. Until next time, thanks for watching. Cheerio.
Hi, I'm Mike, and I'm an AI-aholic. My last big feature, the AI files one I made a whole video about, turned into total slop.
I got so carried away with how fast AI let me build that I never stopped to ask what I was building. I understood the feature at a high level, sure, but ask me about a specific part of the code and I had nothing. Not a comfortable spot, especially for a feature sitting on the critical path for real users.
So I went back in for a serious code cleanup. Here's what I found, what I did about it, and three things you can do today if you're staring at your own pile of AI-generated slop.
What AI files actually does
If you haven't seen the earlier videos: AI files, which shipped in Convex 1.34.0, is part of Convex's broader push on AI coding. When you set up a project, it offers to drop in a set of guidelines and agent skills that help LLMs write better Convex code. There's also a CLI to update those files, remove them, or run a handful of other maintenance tasks. Simple enough as a pitch.
The implementation wasn't. The CLI has to handle a real tangle of paths: first-time convex dev versus later runs, and invocation from the AI files CLI directly versus from the convex dev flow. Then it branches again on who's calling, whether that's a human, an agent, a CI run, or some other non-interactive prompt. That's a lot of conditional logic converging on one entry point, and most of it lived in a single 1,000-line index file. I find code hard to reason about once it looks like that, not because any one line is complicated, but because there's no obvious place to start reading.
Step one: split before you refactor
The first move had nothing to do with rewriting. I broke the monolith into files organized by responsibility: one for agents.md, one for claude.md, one for guideline skills, and so on. I didn't do this by hand. I switched to a smaller, faster model and queued up a series of explicit instructions: split this section off here, that one there. I could probably have handed the model a single high-level goal like "organize this into sensible modules" and gotten a workable result. I did it piece by piece instead, because the overhead was the whole point. Going part by part forced me to look at every corner of the codebase and build a mental map of what existed and where. That map, not the tidier files, was the real payoff, and it was worth the manual effort even though a single prompt could have done the split.
Split Before You Refactor
Untangling a single function
With the map in hand, I turned to the actual logic, starting with one particularly gnarly function. If it made you cross-eyed, you're in good company. That was my reaction the first time I saw it too. Length was the least of its problems. It was doing several unrelated things at once, with nesting and error handling folded in so deep I couldn't tell where one responsibility ended and the next began.
When I hit a function like this, the first question I ask is: what is this actually for? There was a comment at the top: write or update all the Convex AI files, guidelines, agents.md, claude.md, skills. A quick read of the body said the comment wasn't lying, which isn't a given with old comments. From there, the obvious move was to extract each piece into its own sub-function. That alone turned an unreadable block into a short, ordered list of steps, each one named for what it does. It's a small change with a disproportionate payoff. You can understand what the function does at a glance, and change one part without holding the whole thing in your head.
One function worth calling out is readAIConfigOrDefault. The name tells you what it does before you read a line of the body. It calls attemptReadAIState, and the attempt prefix is doing real work. It signals that the call can fail, and that failures are caught inside attemptReadAIState rather than left to blow up the caller. readAIConfigOrDefault handles whatever comes back, success or failure, and decides what "or default" means in that case. Roughly, the pattern looks like this:
1// attemptReadAIState never throws, it captures failure in the return value2asyncfunctionattemptReadAIState(path:string):Promise<AIState | AIStateError>{3try{4returnawaitreadAIState(path);5}catch(err){6return{ kind:"error", reason:toErrorReason(err)};7}8}910// readAIConfigOrDefault decides what "or default" means for each outcome11asyncfunctionreadAIConfigOrDefault(path:string):Promise<AIConfig>{12const result =awaitattemptReadAIState(path);13if(result.kind ==="error"){14returndefaultAIConfig();15}16returntoAIConfig(result);17}18
That split, between what a function does and what its caller does with the result, is one of the biggest things separating readable code from slop.
Three code cleanup habits you can start today
1. Break large functions down
This is the cheapest fix available, and it's almost always worth doing first. Select a chunk of a large function and tell the model to extract it into its own self-contained function. Be explicit, in the prompt or in agents.md, that it needs a descriptive name. Left to its own devices, a model will happily pull out a block of code and call it helper or doStuff. The extraction only pays off if the name documents what the function is for.
2. Segregate query from command
AI-written code (and plenty of human-written code, if we're honest) has a habit of quietly combining a check with an action. Before the refactor, one function was called checkAIFilesStaleness. The name promises a check. The body also logged a message if the files turned out to be stale:
1// Before: the check and the side effect are tangled together2functioncheckAIFilesStaleness(files: AIFiles):void{3if(isOutOfDate(files)){4console.warn(`AI files are stale: ${files.path}`);5}6}7
That's two responsibilities wearing one name, and the honest fix was either renaming it to checkAIFilesStalenessAndLogIfStale, or, better, splitting it outright. I went with the split. determineAIFilesStaleness now does only the check, returning a union of literal staleness states, and the caller handles each state explicitly:
1// After: the query returns a fact, the caller decides what to do with it2typeAIFilesStaleness="upToDate"|"stale"|"missing";34functiondetermineAIFilesStaleness(files: AIFiles): AIFilesStaleness {5if(!files.exists)return"missing";6returnisOutOfDate(files)?"stale":"upToDate";7}89functionhandleAIFilesStaleness(staleness: AIFilesStaleness):void{10switch(staleness){11case"upToDate":12return;13case"stale":14console.warn("AI files are stale, run npx convex ai-files update");15return;16case"missing":17console.warn("No AI files found, run npx convex ai-files add");18return;19default:20 staleness satisfies never;21}22}23
I added the exhaustive check at the end of handleAIFilesStaleness. If a new staleness state is ever introduced, TypeScript's compiler flags every call site that doesn't handle it, so the gap gets caught during a build instead of by a bug report. (Convex's own guide to switch statements covers this exact pattern if you haven't seen it.) That combination of a pure query function plus an exhaustive switch at the boundary makes the control flow legible in a way the original function never was.
3. Name things for what they actually do
This is the throughline of the first two. A function's name is the fastest documentation you'll ever write, and it's read by humans and by the AI agents you'll eventually ask to modify the code. If the name is wrong, if it says "check" when the function also logs, or "read" when the function also mutates state, you're setting up the next person, human or model, to misunderstand the code before they've read a single line of it. If you want more patterns in the same vein, Readable TypeScript code: 14 patterns for humans and AI has a longer list.
Can AI fix the AI slop it made?
Mostly, yes. I turned what I learned from this pass into a Convex de-slop skill you can run against your own codebase for the same kind of code cleanup. I've run it a good number of times now, and it holds up. If you try it, I'd love to know how it goes, and if you spot something it should handle better, open an issue or a PR. I'll probably add more skills like it over time.
For now, that's where I'm leaving this one. If you want another example of Convex being upfront about a design trade-off, Why doesn't Convex have SELECT or COUNT? is worth a read. Thanks for reading. Until next time.
Build in minutes, scale forever.
Convex is the backend platform with everything you need to build your full-stack AI project. Cloud functions, a database, file storage, scheduling, workflow, vector search, and realtime updates fit together seamlessly.