Problem overview
A thermostat keeps a target temperature that must always stay between 10 and 30. If any code could assign the target directly, nothing would stop 95. So the target is a private field, and only the thermostat's own methods can change it.
new Thermostat()starts at 20.setTarget(t)sets the target, clamping values below 10 to 10 and above 30 to 30.warmer()andcooler()move it by 1 but never outside 10 to 30.getTarget()is the only way to read it.
Approach
Put the rule in one place. setTarget clamps its argument. warmer and cooler then call setTarget with the target plus or minus 1, so at 30 the value 31 becomes 30 again, and at 10 the value 9 becomes 10. If the rule were copied into three methods, a later change could easily miss one. Every method runs in constant time.
Step by step
- Private field.
private target: number;Theprivatekeyword makes the compiler rejectthermostat.targetoutside the class. - Constructor sets
this.target = 20. - setTarget, low case. If
t < 10, the target becomes 10. - High case. Else if
t > 30, it becomes 30. - Otherwise the target becomes
t. The values 10 and 30 are allowed, so they land here. - warmer calls
this.setTarget(this.target + 1). - cooler calls
this.setTarget(this.target - 1). - getTarget returns
this.target.
The solution in TypeScript
class Thermostat {
private target: number;
constructor() {
this.target = 20;
}
setTarget(t: number): void {
if (t < 10) {
this.target = 10;
} else if (t > 30) {
this.target = 30;
} else {
this.target = t;
}
}
warmer(): void {
this.setTarget(this.target + 1);
}
cooler(): void {
this.setTarget(this.target - 1);
}
getTarget(): number {
return this.target;
}
}
- Methods can call each other. Inside the class,
this.setTarget(...)is allowed, and so is readingthis.target, even though it is private. privateis a compile-time check. TypeScript erases it when it produces JavaScript, so the property still exists at run time. A learner who wants real run-time privacy can use a#targetfield instead.
Using the class
const h = new Thermostat();
console.log(h.getTarget());
h.warmer();
h.warmer();
console.log(h.getTarget());
h.cooler();
console.log(h.getTarget());
h.setTarget(35);
console.log(h.getTarget());
h.warmer();
console.log(h.getTarget());
h.setTarget(4);
console.log(h.getTarget());
h.cooler();
console.log(h.getTarget());
h.setTarget(10);
h.warmer();
console.log(h.getTarget());
20
22
21
30
30
10
10
11
Writing h.target = 95; outside the class makes the compiler report that the property is private and only accessible within the class Thermostat. Since a type error doesn't stop tsc from writing JavaScript, the assignment would still run, and getTarget() would then return 95. The protection comes from fixing the error, not from the run time.
Why keep the rule in one method
With the rule in setTarget only, there is exactly one place to read when you want to know what values are legal, and exactly one place to change if the limits become 5 and 35. The other methods are built on it and inherit the rule for free. This is the real benefit of encapsulation: the field is private so that the rule cannot be bypassed, and the rule lives in one method so that it cannot drift.
Testing
Push the target against both limits:
- After
setTarget(30), threewarmer()calls leave the target at 30 each time. - After
setTarget(10), acooler()call leaves it at 10. setTarget(-1000)gives 10 andsetTarget(1000)gives 30, the extremes of the constraints.
Another way: clamp with Math.min and Math.max
The three-way if says the rule in words. The same rule fits on one line, by taking the larger of 10 and t and then the smaller of that and 30:
class Thermostat {
private target: number;
constructor() { this.target = 20; }
setTarget(t: number): void { this.target = Math.min(30, Math.max(10, t)); }
warmer(): void { this.setTarget(this.target + 1); }
cooler(): void { this.setTarget(this.target - 1); }
getTarget(): number { return this.target; }
}
const h = new Thermostat();
h.setTarget(35);
console.log(h.getTarget());
h.setTarget(4);
console.log(h.getTarget());
h.setTarget(22);
console.log(h.getTarget());
h.warmer();
console.log(h.getTarget());
This prints 30, 10, 22 and 23. Both forms behave the same. Use whichever you can read at a glance.
Another way: a # field
JavaScript has its own privacy, written #target. Unlike private, it is enforced when the program runs. With a #target field, the compiled code has no property called target at all, so assigning h.target = 95 through an untyped reference creates an unrelated property and getTarget() still returns 20. The lesson uses private, which is the usual TypeScript choice.
Edge cases and common mistakes
- Using
<=or>=. 10 and 30 are valid targets. Clamping them is harmless, but refusing them would be wrong. - Refusing instead of clamping.
setTarget(4)sets 10; it doesn't leave the old value. - Copying the rule. Writing
this.target++inwarmerskips the limit and lets the target reach 31. - Making
getTargetchange something. It must only read.
Extra edge cases
- Repeated presses at a limit. Calling
warmer()three times from 30 keeps the target at 30 each time, andcooler()from 10 keeps it at 10. - Unusual order of calls.
setTarget(35)followed bycooler()gives 29, not 34, because the 35 was clamped to 30 first. - Reading before any change. A new thermostat returns 20 from
getTarget(), since the constructor sets it.
Key points
- Make a field
privatewhen its value must obey a rule, and offer methods that keep the rule. - Put the rule in one method and let the others call it:
warmerandcoolerreusesetTarget. - Clamping means moving an out-of-range value to the nearest allowed one, instead of rejecting it.
- A getter such as
getTarget()lets other code read a private field without being able to change it. - TypeScript's
privateis checked by the compiler, while a#field is also enforced at run time.
All five methods run in constant time: each one compares a number or changes one number.
