Back to Home
Case Study B — Safe TS Migration Pack

nullish-fallback with chain & precedence awareness

Injecting ?? fallback for response.data across a real API client. Not just simple replacement — the engine understands optional chains, operator precedence, and existing nullish coalescing.

The Problem

You have a property that can be undefined at runtime. You need to add ?? fallback to every access site. Sounds simple? Here's why it's not:

Chain access
✗ response.data?.items → response.data ?? null?.items
✓ response.data?.items → (response.data ?? null).items

Naive replacement breaks the chain. The engine wraps the target and preserves the tail.

Operator precedence
✗ price > 100 → price ?? 0 > 100 (parses as price ?? (0 > 100))
✓ price > 100 → (price ?? 0) > 100

?? has lower precedence than >. Without parens, the fallback captures the wrong operand.

Existing ??
✗ data ?? "default" → data ?? null ?? "default"
✓ data ?? "default" → SKIP (already guarded)

The engine detects existing nullish coalescing and classifies it as ambiguous.

4
Scenarios Handled
8
Test Assertions
precedence
Chain Depth
recursive
0
False Positives
Generated Diffs

Four scenarios, four correct transforms.

Scenario 1: Standalone access — simple ?? injection
Standalone access
@src/api/client.ts — standalone access
async function fetchUser(id: string) {
const response = await api.get(`/users/${id}`);
- const name = response.data;
+ const name = response.data ?? null;
return name;
}
Scenario 2: Chain access — wrap target, preserve tail
Chain-aware wrapping
@src/api/client.ts — chain-aware wrapping
function getItems(response: ApiResponse) {
- return response.data?.items;
+ return (response.data ?? null).items;
}
function getMeta(response: ApiResponse) {
- const total = response.data?.meta.total;
+ const total = (response.data ?? null).meta.total;
return total;
}
Scenario 3: Binary expression — add parens for correct precedence
Operator precedence awareness
@src/pricing/calculator.ts — operator precedence
function calculateDiscount(plan: Plan) {
- if (plan.yearlyPrice > 100) {
+ if ((plan.yearlyPrice ?? 0) > 100) {
return "premium";
}
- const total = plan.yearlyPrice + plan.monthlyPrice;
+ const total = (plan.yearlyPrice ?? 0) + plan.monthlyPrice;
return total;
}
Scenario 4: Existing ?? — classified as ambiguous, skipped
Ambiguous: already guarded
@src/utils/defaults.ts — ambiguous: already has ??
// This line is SKIPPED — already has nullish coalescing
const value = response.data ?? "default";
// Classification: ambiguous → no patch generated
Under the Hood
// locate.ts — detect parent context
findOuterChain(node) → walks up PropertyAccessExpression tree
detectBinaryParent(node) → checks if parent is BinaryExpression or ConditionalExpression
// transform.ts — context-aware replacement
if chain(target ?? fallback).tail
if binary(target ?? fallback) // with parens
if standalonetarget ?? fallback // no parens needed

Have nullable properties in your codebase?

Stop adding ?? fallback by hand. Let the engine find every access site and inject the correct guard.