TypeScript Tutorial
TypeScript Encapsulation
Keep fields private. Expose getters and setters so the class can protect its own rules.
Hide the data, publish the rules
Encapsulation means the object owns its fields. Callers do not poke at them. They call methods. The class then decides what is allowed: a score cannot go below zero, a name cannot be empty, a tank cannot hold more than its capacity.
In TypeScript you do this with private data and public getters and setters. The previous chapter introduced access specifiers. This one is why they exist.
Private fields
Put data under private. Code outside the class cannot read or write those names. Methods of the same class still can. That is the whole point: the class is the only place that touches the raw values.
Example
class Player {
private name: string;
private score: number;
constructor(n: string, s: number) {
this.name = n;
this.score = s;
}
public getName(): string {
return this.name;
}
public getScore(): number {
return this.score;
}
}
const p: Player = new Player("Ada", 12);
console.log(p.getName() + " has " + p.getScore());p.score = -4; would not compile. The field is private. You must go through a method if you want to change it.
Getters
A getter returns a copy of a field. Name them getX in this tutorial so the intent is obvious. They let the rest of the program read state without knowing how it is stored.
Later you can change score from a number to something else and keep the same getter. Callers do not notice. TypeScript also has get and set accessors that look like fields from the outside. A later chapter covers that syntax. Here, methods keep the rule visible.
Setters that protect invariants
A setter writes a field only when the new value is valid. If it is not, leave the old value or refuse the change. That is the rule living in one place instead of in every call site.
Example
class Score {
private value: number;
constructor() {
this.value = 0;
}
public get(): number {
return this.value;
}
public add(points: number): void {
if (points < 0) {
return;
}
this.value = this.value + points;
}
}
const s: Score = new Score();
s.add(10);
s.add(-3);
console.log(s.get());The second add is ignored. The score stays 10. If value were public, a caller could set it to anything.
Public data skips the class
| Public field | Private field + methods | |
|---|---|---|
| Who writes it | Anyone with the object | Only the class |
| Checks | You hope every caller remembers | One setter, one rule |
| Rename later | Every use site breaks | Only the class body changes |
Plain objects with public fields are fine for bags of data with no rules. The moment a value has a legal range, make it a class and hide the field.
A tank that cannot overflow
Capacity is set in the constructor. fill never stores more than that. getLiters is the only way to read the current amount.
Example
class Tank {
private liters: number;
private cap: number;
constructor(capacity: number) {
this.cap = capacity;
this.liters = 0;
}
public getLiters(): number {
return this.liters;
}
public fill(amount: number): void {
if (amount < 0) {
return;
}
this.liters = this.liters + amount;
if (this.liters > this.cap) {
this.liters = this.cap;
}
}
}
const t: Tank = new Tank(50);
t.fill(40);
t.fill(20);
console.log(t.getLiters());Output is 50, not 60. The tank enforced its own limit.
Compile this in /typescript/try. Try t.fill(100) on a fresh tank of capacity 50. The printed amount should still be 50.
What to keep private
- Anything that must stay consistent with something else (balance and transactions, size and buffer).
- Anything with a range or format (age, email, percentage).
- Helpers that are not part of the public story of the type.
Getters and setters are not decoration. If a setter only assigns with no check, you still gained the option to add a check later without hunting through the program. Next: a class that reuses another class — inheritance.