Classes: data with rules
Think of a vending machine. You can’t reach in and take a drink, and you can’t set the machine’s cash box to 1,000 EUR. You press buttons (insertCoin, choose), and the machine decides what’s allowed. Its insides are hidden; only the buttons are public.
A class works the same way. It bundles data (properties) with methods that work on that data, and it decides which parts the outside world may touch. Hiding the insides is called encapsulation.
A class is a type with identity, state and behavior; its job is to maintain invariants (rules that are always true for a valid object) across every public operation. Encapsulation is how: the constructor establishes the invariant, private state prevents bypassing it, and each public method preserves it. Swift and Kotlin default to fewer ways to break this (Kotlin classes are final by default; Swift encourages private(set) and value types for plain data); Java and TypeScript need more explicit discipline.
final class Account {
let owner: String
private(set) var balance: Int = 0 // readable outside, changeable only inside
init(owner: String) {
self.owner = owner
}
func deposit(_ amount: Int) {
precondition(amount > 0, "Deposit must be positive")
balance += amount
}
func withdraw(_ amount: Int) -> Bool {
guard amount > 0, amount <= balance else { return false }
balance -= amount
return true
}
}class Account(val owner: String) {
var balance: Int = 0 // readable outside, changeable only inside
private set
fun deposit(amount: Int) {
require(amount > 0) { "Deposit must be positive" }
balance += amount
}
fun withdraw(amount: Int): Boolean {
if (amount <= 0 || amount > balance) return false
balance -= amount
return true
}
}public final class Account {
private final String owner;
private int balance = 0; // hidden; read through a getter
public Account(String owner) {
this.owner = owner;
}
public String owner() { return owner; }
public int balance() { return balance; }
public void deposit(int amount) {
if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive");
balance += amount;
}
public boolean withdraw(int amount) {
if (amount <= 0 || amount > balance) return false;
balance -= amount;
return true;
}
}class Account {
#balance = 0; // # makes it truly private at runtime
constructor(readonly owner: string) {}
get balance(): number {
return this.#balance;
}
deposit(amount: number): void {
if (amount <= 0) throw new Error("Deposit must be positive");
this.#balance += amount;
}
withdraw(amount: number): boolean {
if (amount <= 0 || amount > this.#balance) return false;
this.#balance -= amount;
return true;
}
}The pieces:
- Properties hold the data:
ownerandbalance. - The initializer / constructor (
init, the parentheses afterclass Accountin Kotlin, the method named like the class in Java,constructorin TypeScript) runs when you create an object:Account(owner: "Ana"),Account("Ana"),new Account("Ana"). - Methods are the buttons:
depositandwithdrawcheck the rules before changing anything. - Access control:
balancecan be read from outside but only changed inside the class. No other code can make it negative.
Tap the line with the bug.
final class Account {
var balance: Int = 0
func withdraw(_ amount: Int) -> Bool {
guard amount <= balance else { return false }
balance -= amount
return true
}
}class Account {
var balance: Int = 0
fun withdraw(amount: Int): Boolean {
if (amount > balance) return false
balance -= amount
return true
}
}public class Account {
public int balance = 0;
public boolean withdraw(int amount) {
if (amount > balance) return false;
balance -= amount;
return true;
}
}class Account {
balance = 0;
withdraw(amount: number): boolean {
if (amount > this.balance) return false;
this.balance -= amount;
return true;
}
}Hint: withdraw carefully refuses to go below zero. Is there another way to change the balance?
withdraw protects the rule, but balance is public and writable, so any code can do account.balance = -500 and skip the check entirely. A rule that can be bypassed isn’t a rule. Let outside code read the balance, and change it only through methods:
private(set) var balance: Int = 0var balance: Int = 0
private setprivate int balance = 0;
public int balance() { return balance; }#balance = 0;
get balance(): number { return this.#balance; }Also notice what this version doesn’t check: a negative amount, like withdraw(-100), passes amount <= balance and increases the balance. Every public method has to check every rule.
Computed properties
Some values don’t need to be stored at all, because they can be calculated from other properties every time they’re read:
struct Rectangle {
var width: Double
var height: Double
var area: Double { width * height } // computed on every read
}class Rectangle(var width: Double, var height: Double) {
val area: Double get() = width * height // computed on every read
}record Rectangle(double width, double height) {
double area() { return width * height; } // a method computes it
}class Rectangle {
constructor(public width: number, public height: number) {}
get area(): number { return this.width * this.height; } // computed on every read
}A computed value can never be out of date: change the width, and the area is right the next time anyone reads it. Storing area as a separate property would mean remembering to update it every time.
Classes, structs, records, data classes
For data without behavior or identity, the class is often the wrong tool:
| Need | Swift | Kotlin | Java | TypeScript |
|---|---|---|---|---|
| Plain data, compared by value | struct | data class (immutable vals) | record | type/interface + object literal, readonly |
| Identity and mutable state with rules | final class | class | final class | class with #private fields |
| A single shared instance | static let shared / enum namespace | object | enum singleton or DI-managed bean | module-level const |
Prefer the lightest construct that fits: values for data, classes when there’s an invariant to protect or identity matters. Value types vs reference types covers the copy-vs-share consequences.
Deep diveTypeScript private vs #privateInterview favorite
TypeScript’s private keyword is checked only by the compiler; at runtime it’s an ordinary property, visible in JSON.stringify, in the debugger, and to any JavaScript caller ((obj as any).balance = -1 works). #balance is a JavaScript private field: enforced by the runtime, invisible to Object.keys and serialization, and inaccessible from outside the class body. Use # when the field protects an invariant or holds a secret; private is fine for internal convenience.
- Classes exist to maintain invariants: the constructor establishes them, private state prevents bypass, every public method preserves them (including argument validation).
- Read-only from outside: Swift
private(set), Kotlinprivate set, Java private field + getter, TypeScript#field+ getter (privateis compile-time only). - Use
struct/data class/record/ object types for plain data; classes for identity and guarded state.
Check this part
Why make
balancechangeable only inside the class?What runs when you create a new object?
How do you expose a property as read-only outside the type in Swift?How do you expose a property as read-only outside the class in Kotlin?What does a Java record give you, and what doesn't it?What is the difference between TypeScript
private xand#x?