I started banning it() as an experiment. Every test must use it.each().
Then, I banned try-catch. Every error handler must use catchIfError().
It sounds extreme but I learned that the tighter the constraints on AI, the better the code quality.
_ Note: This is a blogpost about optimisations for AI-generated code specifically in TypeScript._
Why ban?
Have you opened an AI-generated PR recently with 1000 lines of changes but with little substance? You start reviewing and you skim. There are so many lines because AI is really good at writing imperative code in TypeScript. You have no time to review all these lines, so you just let it go…
The better way around this is of course to enforce coding standards that make it easier to review. For me, it’s declarative code , starting with tests.
Tests help with code governance, maintainability, and confidence in your codebase (which in turn helps AI’s confidence). You want AI to generates tests but not the imperative mess that tends to happen with dozens of it() blocks.
Most of your time with AI isn’t about writing code but reviewing it. You need to find ways to review code quickly but also trust it.
Constraint #1: Ban it() and require it.each()
The Problem
If AI generates something like this:
it(’should calculate tax for income of 50000’, () => {
const result = calculateTax(50000);
expect(result).toBe(7500);
});
it(’should calculate tax for income of 100000’, () => {
const result = calculateTax(100000);
expect(result).toBe(18000);
});
it(’should calculate tax for income of 200000’, () => {
const result = calculateTax(200000);
expect(result).toBe(42000);
});
it(’should return 0 for income of 0’, () => {
const result = calculateTax(0);
expect(result).toBe(0);
});
…there’s a lot of repetition. Now imagine you have dozens of test files in a PR. It’s going to get tough to review very quickly.
The Solution
Using it.each():
it.each([
{ income: 50000, expected: 7500 },
{ income: 100000, expected: 18000 },
{ income: 200000, expected: 42000 },
{ income: 0, expected: 0 },
])(’calculateTax($income) = $expected’, ({ income, expected }) => {
expect(calculateTax(income)).toBe(expected);
});
This we can scan in seconds. Reasoning about code is much, much easier. It’s declarative: what, not how.
We’ve created a forcing function for:
Standardized test structure
Makes tests scannable and reliable
Constraint #2: Ban try-catch and require catchIfError()
The problem
If AI generates nested try-catch blocks like this:
async function processUserData(userId) {
try {
const user = await fetchUser(userId);
try {
const profile = await fetchProfile(user.profileId);
try {
const preferences = await fetchPreferences(user.id);
return { user, profile, preferences };
} catch (prefError) {
return { user, profile, preferences: null };
}
} catch (profileError) {
throw new Error(’Could not fetch profile’);
}
} catch (userError) {
throw new Error(’Could not fetch user’);
}
}
…it quickly becomes a nightmare to scan error handling. Human brains (mine especially) are not good at keeping track of layers upon layers. So we need a better method.
The solution
Recently I was introduced to the concept of using error-as-return-value instead of try-catch. It’s an interesting idea (one we see used in e.g. GraphQL), so I ran with it.
async function processUserData(userId) {
const user = await fetchUser(userId);
const profile = await fetchProfile(user.profileId);
const [, preferences] = await catchIfError(fetchPreferences(user.id));
return { user, profile, preferences: preferences ?? null };
}
Its basic implementation could look something like this (you may want to add custom error types and handling):
async function catchIfError(promise) {
try {
const result = await promise;
return [null, result];
} catch (error) {
return [error, null];
}
}
This could be interesting for AI-generated code. It encourages:
Scannable error handling that’s scannable (flat code structure)
Errors as values
Better cohesion , as try blocks become separate functions
On the last point, when you can’t wrap five operations in a try-catch, you’re forced to extract them into a function. Instead of:
try {
// 20 lines of complex logic
} catch (e) {
// handle error
}
You write:
const [error, result] = await catchIfError(doComplexOperation());
This naturally encourages us to write single-responsibility functions.
Make your life easier with constraints
Just like you would guide a junior engineer, add strong guardrails. AI generates better code when you give it constraints.
Instead of Write tests for this function, try Write tests using it.each() with test cases for…
Instead of: Add error handling, try Use catchIfError() for optional operations, let required operations throw.
Other ideas
Ban Magic Numbers
Magic Numbers are bad. Your colleagues won’t understand them and it’s likely your future self won’t remember them either.
Try this: Magic numbers are banned. Extract all numeric literals to named constants at the top of the file”.
// No more magic numbers.
const MAX_RETRY_ATTEMPTS = 3;
const TIMEOUT_MS = 5000;
if (retries > MAX_RETRY_ATTEMPTS) {
throw new Error(’Max retries exceeded’);
}
Ban complex conditionals
Try this: Complex if-statements with more than one boolean operator (&&, ||) are banned. Extract any conditional with more than one boolean operator into a named variable.
// AI generated code.
const isEligibleForDiscount =
user.isActive && user.age > 65 && !user.hasDiscount;
if (isEligibleForDiscount) {
applyDiscount();
}
With each new operator, you’re adding exponential (power of 2) code paths. It’s hard to reason about, so extract the condition to a named variable to make your life easier.
Final thoughts
Optimise code for your review.
Your time is finite so add constraints that make AI-generated code:
Scannable
Standardized
Minimal (declarative wins here)
Thanks for reading!
If you’re experimenting with AI in your workflow, want to share your experiences or want to collaborate, I’d love to hear from you. I write more like this at blog.mariohayashi.com, and feel free to follow me on Twitter: @logicalicy.

Top comments (0)