Think about a cash machine. Behind the little screen sit stacks of banknotes, a secure ledger of your balance, and a lot of careful checking. But you never reach inside it. You press a few buttons — withdraw £20 — and the machine decides whether that's allowed: does the account exist, is there enough money, is the daily limit exceeded? You get to ask for what you want; the machine guards how it's actually done. You could not, even if you tried, lean over and quietly change your balance to a million pounds.
That is encapsulation. In object-oriented programming it means two things bundled into one idea:
The public methods are the object's interface — its buttons. Everything else is hidden machinery. Because nothing outside can poke at the raw data, the object can guarantee its own rules — a bank balance that never goes negative, an email address that is always valid. Those guaranteed-true rules are called invariants, and encapsulation is how an object protects them.
In TypeScript you hide a field by marking it private. A private field can be read and
changed only by code inside the same class — the class's own methods. From outside, it
simply isn't reachable. Compare that with a field marked public (the default), which
anyone can read or overwrite.
Here is a bank account. The balance is private, so the only way to change it is through
deposit and withdraw — and each of those checks the rules first.
The account number is readonly: set once when the object is built, never changed
again. Press Run.
The invariant "the balance is never negative" is now impossible to break from outside. No
matter what the rest of the program does, it can only ever go through withdraw, and
withdraw refuses to overdraw. The object protects itself.
Sometimes the outside world genuinely needs to read or change a piece of state. Encapsulation doesn't forbid that — it routes it. A getter hands back a value (perhaps a computed or copied one); a setter takes a proposed new value and validates it before storing it. The field stays private; the door has a guard on it.
This Person keeps its email private. You can't assign nonsense to it, because the
setter rejects anything without an @:
Notice the naming convention: the private field is _email (leading underscore) and the
public method is getEmail/setEmail. The outside world only ever sees the
methods. That freedom is powerful — you could later decide to store the email encrypted, or split it
into user and domain, and no calling code would need to change, because it never touched the
field directly.
Good encapsulation isn't just "add a getter and setter for every field." The real skill is offering purposeful operations — verbs the object does — rather than laying its fields bare. A well-encapsulated class reads like a set of sensible requests, not a pile of switches.
The user of a Thermostat says warmer or cooler — they never set the
temperature to a raw number. So the safe-range invariant (between 5 and 30 °C) holds no matter how
the object is used. The behaviour is public; the number behind it is private.
On a tiny program written by one person in an afternoon, you might get away with it. But software
grows, gets shared, and lives for years. Every public field is a promise you can never take back:
the moment some far-off piece of code writes account.balance = -50, your careful
rules are worthless, and you can no longer even find everywhere the balance is changed.
Making fields private isn't about distrusting colleagues — it's about making the wrong thing
impossible instead of merely discouraged. The compiler becomes your rule-enforcer.
The classic mistake is to make a field public for convenience — and by doing so,
throw away all your protection. If the balance is public, any line of code anywhere can
write to it directly, sailing straight past your withdraw check:
Your withdraw guard still exists, but it's pointless: nothing forces anyone to use it.
The fix is the whole lesson — make the field private and expose behaviour, not
raw data. Two things to remember: