Logical optimizations
Contents
The second article in the series. The first
was about control flow; this one stays with the same tactic — reshaping code — one layer down, at the condition. Here: merging ifs, factoring shared decisions, and dropping checks that earn nothing. The Boolean algebra of conditions — De Morgan and friends — is a different lever, and gets its own installment next time.
Merging ifs — AKA exportation
A stack of nested ifs:
if (flag1) {
if (flag2) {
if (flag3) {
// actual code
}
}
}
is just a conjunction:
if (flag1 && flag2 && flag3) {
// actual code
}
Exportation in formal logic is the same move: a chain of nested conditions pulled out into a single conjunction.
This pattern shows up all the time, usually an artifact of several refactors. It’s only valid when each if is the whole body of the one above it — no else branches, no trailing code — otherwise the conjunction changes behavior.
Reducing decisions
An if comes with an else for free: testing a condition once already splits the world into its true and false halves, so re-checking the negation later is wasted work.
Two ifs that pivot on A:
if (A && B) { /* code #1 */ }
if (!A && C) { /* code #2 */ }
becomes:
if (A) {
if (B) { /* code #1 */ }
} else {
if (C) { /* code #2 */ }
}
Now A is evaluated once. That matters: A, B, C are arbitrary expressions — possibly expensive, possibly side-effecting — not cheap booleans. It also removes the temptation to hoist A into a local just to test it once.
Trivial simplifications and design decisions
As an example we’ll use actual code written by an AI agent (lightly simplified from the original):
const makeWrapper = ({tmpDir, createWrapper}) => {
if (tmpDir !== undefined && createWrapper !== undefined) {
throw new TypeError('sort: pass `tmpDir` OR `createWrapper`, not both');
}
if (createWrapper !== undefined) {
if (typeof createWrapper !== 'function')
throw new TypeError('sort: `createWrapper` must be a function');
return createWrapper();
}
if (tmpDir !== undefined) {
if (typeof tmpDir !== 'string' || !tmpDir)
throw new TypeError('sort: `tmpDir` must be a non-empty string');
return defaultCreateWrapper(tmpDir);
}
throw new TypeError('either `tmpDir` or `createWrapper` is required');
};
Mechanical reductions
First pass: pattern recognition only, no design work yet. The code above is mostly visual noise. Let’s strip the !== undefined checks and lean on truthiness: a valid tmpDir is a non-empty string and a valid createWrapper is a function, both truthy — so on any input the function actually supports, truthiness and !== undefined agree. The first if also looks redundant — we can fold it into the createWrapper branch, testing createWrapper once instead of twice (the Reducing decisions
move again). Applying both:
const makeWrapper = ({tmpDir, createWrapper}) => {
if (createWrapper) {
if (typeof createWrapper !== 'function')
throw new TypeError('sort: `createWrapper` must be a function');
if (tmpDir)
throw new TypeError('sort: pass `tmpDir` OR `createWrapper`, not both');
return createWrapper();
}
// createWrapper not specified here
if (tmpDir) {
if (typeof tmpDir !== 'string')
throw new TypeError('sort: `tmpDir` must be a non-empty string');
return defaultCreateWrapper(tmpDir);
}
throw new TypeError('either `tmpDir` or `createWrapper` is required');
};
Same behavior on every valid input, less noise. (Truthiness does fold falsy-but-defined values — null, 0, '' — into the not specified branch, but those were never valid arguments: still rejected, only the error message differs. Mechanical, not a design decision.) Every further change from here alters what the function actually does.
Where to cut corners
Second pass: every change from here on is a deliberate trade-off, not a syntactic rewrite. Let’s ask what each check actually buys us:
- What if the user passes both
createWrapperandtmpDir— do we really need to refuse to run? Reformulate with the developer experience in mind:createWrappertakes priority. If it’s specified, that’s what the user wants, and we can ignoretmpDir. - We check that
createWrapperis a function. Why? To produce a nicer error message. But what happens if it isn’t a function? It throws on first use with a message saying so. Either way the user learns the same thing. The only real difference is where the failure surfaces: at the call site or at the use site. That’s a judgement call, and it depends on the shape of the surrounding code. If we savecreateWrapperfor use later — especially asynchronously, far from the call — checking up front makes sense. If it blows up in the same function, as in this example, the eager check earns nothing. - And we check it’s a function, but never its signature — what’s the point of one without the other?
- We check the type of
tmpDirbefore passing it todefaultCreateWrapper(). But isn’t it the function’s job to validate its own arguments? Let’s assume it is.
Most of these checks would be unnecessary in TypeScript. But our example is plain JavaScript, which calls for different techniques.
Let’s strip them:
const makeWrapper = ({tmpDir, createWrapper}) => {
if (createWrapper) return createWrapper();
if (tmpDir) return defaultCreateWrapper(tmpDir);
throw new TypeError('either `tmpDir` or `createWrapper` is required');
};
We can cut it even further (assuming defaultCreateWrapper() validates tmpDir itself):
const makeWrapper = ({tmpDir, createWrapper}) =>
createWrapper ? createWrapper() : defaultCreateWrapper(tmpDir);
Note this is a behavior change, not a pure simplification — we deliberately chose a more permissive policy.
Instead of being super-pedantic, we shrink the code until what it does is painfully obvious.
Summary
Same lever as the previous installment
, one layer down: control flow there, the shape of conditions here. Merge nested ifs into a single conjunction; let one test hand you its else for free; drop the checks that can be avoided. And don’t force non-linear logic into an else if chain — each branch silently carries the negation of every condition above it.
None of these is a major win on its own. Long-tail discipline, same as the linearization piece: no individual move buys you much; the compounding does.
Next time, the other lever — the Boolean algebra of the conditions themselves: De Morgan’s laws, and a worked example that turns a tangle of flags into a line you can read.