Refactoring improves code structure without changing behavior. This guide covers techniques for safe, effective refactoring.
When to Refactor#
Rule of Three#
1// First time: Just do it
2function calculateTaxUS(amount: number): number {
3 return amount * 0.08;
4}
5
6// Second time: Note the duplication
7function calculateTaxCA(amount: number): number {
8 return amount * 0.13;
9}
10
11// Third time: Refactor
12function calculateTax(amount: number, rate: number): number {
13 return amount * rate;
14}
15
16const TAX_RATES = {
17 US: 0.08,
18 CA: 0.13,
19 UK: 0.20,
20};Before Adding Features#
1// Before: Hard to add new payment method
2function processPayment(type: string, amount: number) {
3 if (type === 'credit') {
4 // 50 lines of credit card logic
5 } else if (type === 'paypal') {
6 // 50 lines of PayPal logic
7 }
8 // Adding 'apple_pay' means more conditions
9}
10
11// After: Easy to extend
12interface PaymentProcessor {
13 process(amount: number): Promise<PaymentResult>;
14}
15
16class CreditCardProcessor implements PaymentProcessor { }
17class PayPalProcessor implements PaymentProcessor { }
18class ApplePayProcessor implements PaymentProcessor { } // Easy to addSafe Refactoring Process#
1. Ensure Test Coverage#
1// Before refactoring, add tests for current behavior
2describe('calculateOrderTotal', () => {
3 it('should calculate subtotal correctly', () => {
4 const order = createOrder([
5 { price: 10, quantity: 2 },
6 { price: 5, quantity: 3 },
7 ]);
8 expect(calculateOrderTotal(order).subtotal).toBe(35);
9 });
10
11 it('should apply percentage discount', () => {
12 const order = createOrder([{ price: 100, quantity: 1 }]);
13 order.discount = { type: 'percentage', value: 10 };
14 expect(calculateOrderTotal(order).total).toBe(90);
15 });
16
17 // Cover all edge cases before refactoring
18});2. Small, Incremental Changes#
1// Step 1: Extract variable
2const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0) * (1 - discount / 100);
3
4// Becomes:
5const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
6const discountAmount = subtotal * (discount / 100);
7const total = subtotal - discountAmount;
8
9// Step 2: Extract function (separate commit)
10function calculateSubtotal(items: Item[]): number {
11 return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
12}
13
14function applyDiscount(amount: number, discountPercent: number): number {
15 return amount * (1 - discountPercent / 100);
16}
17
18const total = applyDiscount(calculateSubtotal(items), discount);3. Run Tests After Each Change#
# After every small change
npm test
# Or with watch mode
npm test -- --watchCommon Refactoring Patterns#
Extract Method#
1// Before
2function printOwing() {
3 let outstanding = 0;
4
5 console.log("***********************");
6 console.log("**** Customer Owes ****");
7 console.log("***********************");
8
9 for (const order of orders) {
10 outstanding += order.amount;
11 }
12
13 console.log(`name: ${name}`);
14 console.log(`amount: ${outstanding}`);
15}
16
17// After
18function printOwing() {
19 printBanner();
20 const outstanding = calculateOutstanding();
21 printDetails(outstanding);
22}
23
24function printBanner() {
25 console.log("***********************");
26 console.log("**** Customer Owes ****");
27 console.log("***********************");
28}
29
30function calculateOutstanding(): number {
31 return orders.reduce((sum, order) => sum + order.amount, 0);
32}
33
34function printDetails(outstanding: number) {
35 console.log(`name: ${name}`);
36 console.log(`amount: ${outstanding}`);
37}Replace Conditional with Polymorphism#
1// Before
2function calculatePay(employee: Employee): number {
3 switch (employee.type) {
4 case 'engineer':
5 return employee.salary;
6 case 'salesman':
7 return employee.salary + employee.commission;
8 case 'manager':
9 return employee.salary + employee.bonus;
10 default:
11 throw new Error('Unknown employee type');
12 }
13}
14
15// After
16abstract class Employee {
17 constructor(protected salary: number) {}
18 abstract calculatePay(): number;
19}
20
21class Engineer extends Employee {
22 calculatePay(): number {
23 return this.salary;
24 }
25}
26
27class Salesman extends Employee {
28 constructor(salary: number, private commission: number) {
29 super(salary);
30 }
31
32 calculatePay(): number {
33 return this.salary + this.commission;
34 }
35}
36
37class Manager extends Employee {
38 constructor(salary: number, private bonus: number) {
39 super(salary);
40 }
41
42 calculatePay(): number {
43 return this.salary + this.bonus;
44 }
45}Introduce Parameter Object#
1// Before
2function amountInvoiced(startDate: Date, endDate: Date): number { }
3function amountReceived(startDate: Date, endDate: Date): number { }
4function amountOverdue(startDate: Date, endDate: Date): number { }
5
6// After
7class DateRange {
8 constructor(
9 readonly start: Date,
10 readonly end: Date
11 ) {}
12
13 contains(date: Date): boolean {
14 return date >= this.start && date <= this.end;
15 }
16}
17
18function amountInvoiced(range: DateRange): number { }
19function amountReceived(range: DateRange): number { }
20function amountOverdue(range: DateRange): number { }Replace Magic Numbers with Constants#
1// Before
2if (user.age >= 18 && user.creditScore > 650) {
3 if (loanAmount <= user.income * 0.3) {
4 approveLoan();
5 }
6}
7
8// After
9const MINIMUM_AGE = 18;
10const MINIMUM_CREDIT_SCORE = 650;
11const MAX_LOAN_TO_INCOME_RATIO = 0.3;
12
13if (user.age >= MINIMUM_AGE && user.creditScore > MINIMUM_CREDIT_SCORE) {
14 const maxLoanAmount = user.income * MAX_LOAN_TO_INCOME_RATIO;
15 if (loanAmount <= maxLoanAmount) {
16 approveLoan();
17 }
18}Decompose Conditional#
1// Before
2if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
3 charge = quantity * winterRate + winterServiceCharge;
4} else {
5 charge = quantity * summerRate;
6}
7
8// After
9if (isSummer(date)) {
10 charge = summerCharge(quantity);
11} else {
12 charge = winterCharge(quantity);
13}
14
15function isSummer(date: Date): boolean {
16 return !date.before(SUMMER_START) && !date.after(SUMMER_END);
17}
18
19function summerCharge(quantity: number): number {
20 return quantity * summerRate;
21}
22
23function winterCharge(quantity: number): number {
24 return quantity * winterRate + winterServiceCharge;
25}IDE Refactoring Tools#
1// Most IDEs support:
2// - Rename (F2 in VS Code)
3// - Extract Method/Function
4// - Extract Variable
5// - Inline Variable
6// - Move to File
7// - Change Signature
8
9// VS Code: Right-click → Refactor
10// Or: Cmd/Ctrl + Shift + RWhen NOT to Refactor#
- Deadline is tomorrow (ship first, refactor later)
- Code will be deleted soon
- No tests and can't add them
- You don't understand the code yet
- Major rewrite is more practicalConclusion#
Refactoring is a skill that improves with practice. Always have tests before refactoring, make small changes, and run tests frequently. Good refactoring makes future changes easier and code more enjoyable to work with.