Clean Code

Decoupling Functions and Methods: Best Practices in TypeScript

I was staring at a 200-line function called processOrder. It calculated subtotals, applied discounts, computed tax, validated inventory, updated stock, se...

19 Apr 2024

Decoupling Functions and Methods: Best Practices in TypeScript

I was staring at a 200-line function called processOrder. It calculated subtotals, applied discounts, computed tax, validated inventory, updated stock, sent a confirmation email, and logged analytics. One function. All of it.

Testing it required setting up a database, an email server, and an analytics endpoint. Changing the tax calculation meant risking the email logic. Everything was fused together.

What decoupling functions means

Decoupling is breaking a large function into smaller, focused pieces. Each piece does one thing. Each piece can be tested, reused, and changed independently.

It's not about making functions tiny for the sake of it. It's about making each function trustworthy — when you read its name, you know exactly what it does and what it doesn't do.

Before: a coupled function

Typescript
function calculateOrderTotal(items: Item[], taxRate: number): number {
  let subtotal = 0;
  for (const item of items) {
    subtotal += item.price * item.quantity;
  }
  const tax = subtotal * taxRate;
  return subtotal + tax;
}

This isn't terrible, but the subtotal logic and tax logic are tangled. You can't reuse the subtotal calculation elsewhere. You can't test tax logic without also computing a subtotal.

After: decoupled functions

Typescript
function calculateSubtotal(items: Item[]): number {
  return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}

function calculateTax(subtotal: number, taxRate: number): number {
  return subtotal * taxRate;
}

function calculateOrderTotal(items: Item[], taxRate: number): number {
  const subtotal = calculateSubtotal(items);
  const tax = calculateTax(subtotal, taxRate);
  return subtotal + tax;
}

Now each function has one job. You can test calculateTax with any subtotal value. You can reuse calculateSubtotal in a receipt generator. The orchestrating function reads like a summary.

Guidelines for splitting functions

Follow the Single Responsibility Principle. If a function does two things, it should be two functions.

Extract when you see a comment. A comment like // calculate tax inside a larger function is a sign that block should be its own function. Let the function name replace the comment.

Pass data in, return data out. Decoupled functions don't reach into global state. They take inputs and produce outputs. That's it.

Name the extracted function by what it does. If you can't name it clearly, the extraction might not be right. Go back and find a cleaner boundary.

The trade-off

More functions means more names to invent, more files to navigate, and more indirection when tracing execution. If you split too aggressively, you end up with dozens of one-liner functions that add noise without adding clarity.

The sweet spot: split when a function does multiple things. Keep it together when the logic is cohesive and simple. Use your judgment. If a teammate would need to scroll or re-read to understand the function, it's too big.