Method-Chaining Calculator
Build a calculator object with add, subtract, multiply, divide, and a terminal value. Mutable this vs immutable copies, and divide-by-zero as an API choice.
- javascript
- oop
- chaining
- interview
The prompt is calc.add(5).multiply(2).subtract(3).value() and they want a number. People either forget to return anything (chain dies after add) or they return this.value from every method (chain dies with a number in the middle). Both fail the same way: the next identifier is not a calculator.
A method-chaining calculator is an object whose mutating operations return a calculator, plus a terminal read that returns a number. The design fork is whether that returned calculator is the same object (return this) or a new one. Interviews usually want this. Production code that shares the instance across callers often wants copies.
Divide-by-zero is not a math trivia question. IEEE 754 gives Infinity. Your API may still throw. Pick one and say it before you type.
- Chain methods return a calculator. The terminal method (or getter) returns the number.
return thismutates one instance. Returningnew Calculator(next)is referentially transparent.- Interviews usually ask for
this. Say the tradeoff in one sentence.- Divide by zero: throw, or return
Infinity/-Infinity/NaNas the spec’s/would.- Don’t hide the total behind a
get value()unless you also keep avalue()method if the prompt used parens.
Return this, or return a new calculator
Fluent APIs work because each step has a receiver that still has the methods. this is that receiver for ordinary methods. Arrows as class fields would freeze this to the instance (often fine) or to undefined in a loose object literal (not fine). Use methods.
Mutable: return this
function createCalculator(initial = 0) {
let value = initial;
const api = {
add(n) {
value += n;
return api;
},
subtract(n) {
value -= n;
return api;
},
multiply(n) {
value *= n;
return api;
},
divide(n) {
if (n === 0) throw new RangeError('Division by zero');
value /= n;
return api;
},
value() {
return value;
},
};
return api;
}
createCalculator(10).add(5).multiply(2).subtract(4).divide(2).value();
// 13
A class is the same contract with this._value instead of a closed-over value. The factory above avoids leaking _value as a writable field. Classes are fine if the interviewer started with class.
class Calculator {
constructor(initial = 0) {
this._value = initial;
}
add(n) {
this._value += n;
return this;
}
subtract(n) {
this._value -= n;
return this;
}
multiply(n) {
this._value *= n;
return this;
}
divide(n) {
if (n === 0) throw new RangeError('Division by zero');
this._value /= n;
return this;
}
value() {
return this._value;
}
}
new Calculator(2).add(3).multiply(4).value(); // 20
Alias hazard: const a = new Calculator(1); const b = a.add(1); then a and b are the same object. b.multiply(10) changes what a.value() returns. That is the cost of return this.
Immutable: return a new calculator
class ImmutableCalc {
constructor(initial = 0) {
this._value = initial;
}
add(n) {
return new ImmutableCalc(this._value + n);
}
subtract(n) {
return new ImmutableCalc(this._value - n);
}
multiply(n) {
return new ImmutableCalc(this._value * n);
}
divide(n) {
if (n === 0) throw new RangeError('Division by zero');
return new ImmutableCalc(this._value / n);
}
value() {
return this._value;
}
}
const start = new ImmutableCalc(1);
const plusOne = start.add(1);
start.value(); // 1
plusOne.value(); // 2
Each step allocates. Chains with undo, React state, or “pass this snapshot to a child” want this shape. See immutability patterns. On a 20-minute whiteboard, return this is what they are scoring unless they said “don’t mutate.”
| Style | add returns |
Alias safety | Typical prompt |
|---|---|---|---|
| Mutable | this |
no | “chain add/multiply, then value” |
| Immutable | new instance | yes | “each call returns a new calculator” |
Divide by zero is an API choice
JavaScript’s / does not throw:
1 / 0; // Infinity
-1 / 0; // -Infinity
0 / 0; // NaN
That is IEEE 754, covered in number precision. A calculator that silently returns Infinity will then multiply(2) into more infinities and look “working” until a UI prints Infinity. Throwing RangeError fails the chain immediately. Either is defensible. Mixing them (throw on 0, but not on 0 / 0 via a previous NaN) is how you look unsure.
If you allow NaN inputs, every later operation stays NaN. Reject non-finite n at the boundary if the prompt is a numeric keypad.
Terminal read
Prompts vary:
.value()method.equals()/.result()- getter
get value()so.valuehas no parens
A getter is fine (getters and setters), but calc.value without calling it still needs to be a number, and you lose a place to put breakpoints. If the example in the prompt has (), write a method.
Don’t toString the object into a number unless they asked for +calc or template interpolation. That is the infinite currying trick, not this API.
Calculator footguns
- Returning
this._valuefromadd. Chain breaks. Return the object. - Using arrows on an object literal.
add: (n) => { this._value += n; return this; }takesthisfrom the enclosing scope, not the calculator. - Floats.
0.1 + 0.2is not0.3. Integer money does not belong in this calculator. - Order.
add(1).multiply(2)is notmultiply(2).add(1). Chain order is evaluation order, left to right, no operator precedence between methods. - Sharing a mutable instance from a module.
export const calc = new Calculator()is a global. Two features chaining it will clobber each other.
Interview out loud
“Each of add, subtract, multiply, and divide updates the total and returns the calculator so the next method exists. value() is the terminal read and returns a number. I will mutate this and return this unless you want immutable snapshots, in which case each method returns a new instance. Divide by zero: I throw RangeError rather than handing back Infinity, and I will switch if you prefer IEEE behavior. I will not use arrows for the methods, because this has to be the calculator. Alias risk: two variables holding the mutable instance are the same object.”
Further reading
Related on this site
- Classes in JavaScript
- this binding rules
- Getters and setters
- Immutability patterns in JS
- Number precision and IEEE 754
- Error types and custom errors
- Infinite currying in JavaScript
Related guides
- Analytics SDK Event QueueQueue analytics events, send them in series, retry the ones that fail, and flush on page hide without dropping the last click.
- Arrays That Emit Change EventsWrap push, pop, and splice so subscribers hear adds and removes. Proxy vs subclass, and why mutating via index assignment still slips through.
- Array.filter PolyfillWrite filter that skips holes, respects thisArg, and never writes empty slots for rejected items. Contrast with map’s hole-preserving result.
- Array.find PolyfillWrite Array.prototype.find: first callback hit wins, holes are read as undefined, nothing matching returns undefined, and findIndex is the index twin.
- Array.flat PolyfillImplement flat(depth) with Infinity, holes, and non-array array-likes. Recursion vs stack, and why concat(spread) blows up on huge inputs.