Long Function
One function contains several conceptual steps or levels of abstraction.
An easy-to-scan guide to the core ideas in Martin Fowler’s Refactoring, with Kent Beck and contributors—translated into modern TypeScript examples and day-to-day engineering habits.
The point is not aesthetic perfection. The point is making tomorrow’s feature, fix, or explanation cheaper and safer.
Refactoring changes the structure of code while preserving what users, callers, and tests can observe.
Improve the internal design of working code through a sequence of tiny, behavior-preserving transformations.
It is not a rewrite, feature addition, or bug fix—even though it often makes features and fixes dramatically easier.
Establish the behavior and prove the path end-to-end.
Reveal intent, remove duplication, and restore boundaries.
Shape the design around what you now understand.
Speed comes from feedback. Keep every step small enough that a failure has only one likely cause.
Add or locate a test at the observable boundary.
Choose a concrete design problem, not “clean it up.”
Extract, rename, move, inline, or encapsulate.
Tests, types, lint, and a focused runtime check.
Create a safe point with a narrow, truthful change.
Let the improved design reveal the next move.
If the tests fail, the last move should be small enough to understand or reverse immediately.
Add a feature, fix a defect, or change an output.
Rename, extract, move, simplify, or encapsulate.
A smell is a reason to investigate, not a verdict. Search by symptom, then choose the smallest useful response.
One function contains several conceptual steps or levels of abstraction.
The same knowledge or calculation has multiple representations.
A reader must inspect implementation or comments to learn what a name means.
Arguments travel together or force callers to know too many details.
A class changes for unrelated reasons or hides several distinct concepts.
A function works more with another object’s data than with its own.
The same group of values repeatedly appears together.
Strings and numbers carry domain rules that the type system cannot see.
The same type-based branching is copied across the system.
Callers navigate through a long chain of internal object relationships.
A module mostly forwards calls and no longer adds meaning or protection.
One business change requires small edits across many unrelated files.
Shared state changes from multiple places, making order and ownership unclear.
Comments explain confusing structure rather than valuable intent or constraints.
Open a card for the before-and-after code. These are small transformations; their leverage comes from repetition.
Turn an explainable block into a named concept.
Extract by purpose, not length. The resulting name should explain why the block exists and keep each function at one level of abstraction.
function printInvoice(invoice: Invoice) {
let total = 0;
for (const line of invoice.lines) {
total += line.quantity * line.price;
}
console.log(`Invoice ${invoice.id}`);
console.log(`Total: ${total}`);
}function invoiceTotal(invoice: Invoice): number {
return invoice.lines.reduce(
(sum, line) => sum + line.quantity * line.price,
0,
);
}
function printInvoice(invoice: Invoice) {
console.log(`Invoice ${invoice.id}`);
console.log(`Total: ${invoiceTotal(invoice)}`);
}Turn recurring arguments into a domain concept.
A parameter object reduces signature churn and creates a natural home for validation and behavior. Avoid using it as an unstructured “options bag.”
function ordersInRange(
orders: Order[],
start: Date,
end: Date,
timezone: string,
): Order[] {
// filtering logic
}type DateRange = Readonly<{
start: Date;
end: Date;
timezone: string;
}>;
function ordersInRange(
orders: readonly Order[],
range: DateRange,
): Order[] {
// filtering logic
}Make domain rules visible to code and compiler.
A branded type prevents accidental mixing; a value object can additionally own parsing, normalization, and validation. Choose the lightest form that protects a real invariant.
function sendReceipt(email: string) {
if (!email.includes("@")) {
throw new Error("Invalid email");
}
// ...
}type Email = string & { readonly __brand: "Email" };
function parseEmail(value: string): Email {
if (!value.includes("@")) {
throw new Error("Invalid email");
}
return value as Email;
}
function sendReceipt(email: Email) { /* ... */ }Localize behavior that varies by type.
Use this when the same branching repeats or each branch is substantial. A single exhaustive switch over a discriminated union is often clearer than premature class hierarchies.
function shippingCost(order: Order): number {
switch (order.shipping) {
case "standard": return 5;
case "express": return 15;
case "pickup": return 0;
}
}interface ShippingMethod {
cost(order: Order): number;
}
class ExpressShipping implements ShippingMethod {
cost(order: Order): number {
return order.total > 100 ? 8 : 15;
}
}
const cost = order.shippingMethod.cost(order);Make state changes explicit and unsurprising.
Queries answer questions; commands change state. Separating them improves reuse, testing, caching, and reasoning about side effects.
function findAndNotifyOverdue(
invoices: Invoice[],
): Invoice[] {
const overdue = invoices.filter(isOverdue);
overdue.forEach(sendReminder);
return overdue;
}function findOverdue(
invoices: readonly Invoice[],
): Invoice[] {
return invoices.filter(isOverdue);
}
function notifyOverdue(invoices: Invoice[]): void {
invoices.forEach(sendReminder);
}Types are a powerful refactoring assistant, but passing the compiler is not the same as preserving runtime behavior.
Rename symbols through language tooling, tighten a signature, then follow errors to every affected caller. Compiler errors are a work queue—not proof of correctness.
Replace loose booleans and optional fields with explicit variants. Impossible combinations should be difficult or impossible to construct.
readonlyUse readonly parameters and return fresh values when mutation is not part of the contract. This narrows the places where state can change unexpectedly.
Keep unknown at system boundaries. Validate and convert it into trusted domain types before business logic touches it.
asA cast silences the compiler without improving the design. Prefer type guards, parsers, constructors, or better data flow.
tsc --noEmit catches structural mismatches. Tests protect behavior. A focused runtime check catches integration assumptions neither can see.
A realistic sequence matters more than a dramatic before/after. Here is how to improve a pricing function without mixing in a pricing change.
A checkout function calculates a subtotal, applies two discounts, adds shipping, and formats the result. We need to add a loyalty rule next—but first, make the existing rules visible.
Cover a normal cart, the free-shipping threshold, the coupon path, and their boundary values. Assert the current totals—even if the implementation looks odd.
expect(checkoutTotal(cartAtThreshold, noCoupon)).toBe(100);
expect(checkoutTotal(cartBelowThreshold, noCoupon)).toBe(104.99);Extract subtotal, couponDiscount, and shippingCost. Keep the original order of operations and numeric behavior exactly as they are.
const beforeShipping = subtotal(cart) - couponDiscount(cart, coupon);
return beforeShipping + shippingCost(beforeShipping);Replace the coupon string and scattered percentages with Coupon and Money concepts only if they protect real invariants. Rename threshold constants by business meaning.
Execute the focused tests, typecheck, lint the touched files, and compare a few real checkout scenarios. Commit the structural change separately.
Now the feature becomes a small, explicit change. Add new tests for the loyalty behavior, then implement it in the place the refactoring revealed.
Refactoring is usually woven into product work. Its value is highest immediately before the design would otherwise slow down a real change.
Refactor when it makes a concrete change easier—not merely because code looks old.
Good moments include preparing for a feature, exposing a bug, removing the third repetition, or clarifying code during review.
Choose the simplest clear solution. Do not predict every future variation.
Compare the similarities and differences. Duplication may still be accidental.
The repeated knowledge is now concrete enough to extract responsibly.
Refactor toward a named, tested seam that makes the intended change local.
Add coverage, narrow the target, or leave stable code alone. “Cleaner” is not a sufficient business case.
Use this lightweight sequence when you feel the urge to reorganize code.
A compact routine for real pull requests and pairing sessions.
The best design makes a business change local.
When one rule changes, one clear place should usually change with it. That is the practical measure of maintainability.