This topic is the practical "how-to" that follows the theory of complexity metrics. It focuses on the specific surgical techniques you can use to dismantle "spaghetti code" and turn high-complexity functions into clean, readable ones. see previous article here
Knowing that your code is complex is only half the battle. The real skill lies in the refactor, the ability to take a function with a high cognitive complexity score and simplify it without changing its behavior.
In TypeScript and Dart, we have unique language features that make this process easier. Here are four essential strategies to lower your complexity scores today.
1. The Power of the Guard Clause
The "Arrow Anti-pattern" (nested if statements) is the biggest contributor to high cognitive complexity. Guard clauses allow you to "fail fast" and keep the main logic at the lowest nesting level.
Before (Deep Nesting):
function getDiscount(user: User) {
if (user.isMember) {
if (user.active) {
if (user.points > 100) {
return 0.2;
}
}
}
return 0;
}
After (Linear Flow):
function getDiscount(user: User) {
if (!user.isMember || !user.active) return 0;
if (user.points <= 100) return 0;
return 0.2;
}
Impact: Reduced Cognitive Complexity from 5 to 2.
2. Extracting to "Composed Functions"
If a function is doing multiple things, it naturally accumulates a high score. Break it down into small, private helper functions that do one thing well.
Strategy: If you see a comment like // Step 2: Calculate Taxes, that block of code belongs in its own function.
In Dart:
// Before
void processOrder(Order order) {
// Validate
if (order.id.isEmpty) throw Exception();
// Calculate
double total = order.items.fold(0, (sum, item) => sum + item.price);
// Save
repository.save(order);
}
// After
void processOrder(Order order) {
_validateOrder(order);
final total = _calculateTotal(order);
repository.save(order, total);
}
3. Replace Complex Switch/If with Lookups
In TypeScript, you can often replace a bulky switch statement with a Record or an Object literal. This removes the "branching" complexity entirely.
Before:
function getColor(status: Status) {
switch (status) {
case 'success': return 'green';
case 'error': return 'red';
case 'pending': return 'yellow';
default: return 'gray';
}
}
After (The Configuration Map):
const STATUS_COLORS: Record<Status, string> = {
success: 'green',
error: 'red',
pending: 'yellow',
};
function getColor(status: Status) {
return STATUS_COLORS[status] ?? 'gray';
}
4. Modern Language Features (Dart & TS)
Use modern syntax to handle nulls and collections. This removes the need for explicit if checks.
Optional Chaining (TS/Dart): Use user?.profile?.address instead of nested if (user && user.profile).
Nullish Coalescing: Use const name = inputName ?? 'Guest' instead of ternary operators.
Collection If/For (Dart): Dart allows you to use logic directly inside list literals, which is much cleaner than building lists imperatively.
// Dart Collection If
final navBar = [
HomeButton(),
if (isAdmin) AdminSettings(),
ProfileButton(),
];
Summary: The Refactoring Checklist
When you see a complexity score above 10, ask these three questions:
- Can I return early? (Flatten the nesting).
- Can I name this block? (Extract to a new function).
- Is this logic data or code? (Replace if/else with a Map/Record).
Conclusion
Refactoring for readability isn't about making the code shorter, it's about making it predictable. When a developer can read a function from top to bottom without jumping back and forth between nested braces, the "mental tax" of maintaining that code drops to near zero.
p.s: The examples here may be too simplified, but the idea is to establish the underlying patterns.
Top comments (0)