TypeScript Fundamentals: Types, Generics and Real-World Patterns · TypeScript Basics: Types, Narrowing and Functions
Classes: Modifiers, Interfaces and Abstract Types
Write classes with typed properties, parameter properties and access modifiers, implement interfaces, use abstract classes and getters, and understand private versus #private.
What you'll build
A small bank: an abstract Account with a protected balance, a SavingsAccount that adds interest, a read-only account number, and a Transferable interface that any class can implement.
The idea
TypeScript classes are JavaScript classes plus types and modifiers.
- Property declarations: declare the type of every property in the class body.
- Parameter properties:
constructor(public readonly owner: string)declares and assigns a property in one line. - Access modifiers:
public(default),protected(class and subclasses),private(class only). These are checked at compile time.#field(ECMAScript private) is enforced at runtime too. readonly: assigned only in the declaration or constructor.implements: a class promises to satisfy an interface; the compiler checks it.abstractclasses cannot be instantiated and can declare abstract methods that subclasses must implement.- Getters and setters (
get balance()) expose computed or guarded properties. override(with thenoImplicitOverrideoption) makes the compiler check that a method really overrides one in the parent class.
Prefer composition over deep inheritance: interfaces and small objects are easier to test than class trees.
The code
interface Transferable {
transferTo(target: Account, amount: number): void;
}
abstract class Account implements Transferable {
static count = 0;
readonly number: string;
#history: string[] = []; // truly private at runtime
protected balance = 0;
constructor(public readonly owner: string, opening = 0) {
Account.count++;
this.number = `ACC-${String(Account.count).padStart(3, "0")}`;
this.deposit(opening);
}
abstract monthlyFee(): number;
get summary(): string {
return `${this.number} ${this.owner}: ${this.balance.toFixed(2)}`;
}
deposit(amount: number): void {
if (amount < 0) throw new RangeError("amount must be positive");
this.balance += amount;
this.#history.push(`+${amount}`);
}
withdraw(amount: number): void {
if (amount > this.balance) throw new Error("insufficient funds");
this.balance -= amount;
this.#history.push(`-${amount}`);
}
transferTo(target: Account, amount: number): void {
this.withdraw(amount);
target.deposit(amount);
}
get history(): readonly string[] {
return this.#history;
}
}
class CheckingAccount extends Account {
monthlyFee(): number {
return 5;
}
}
class SavingsAccount extends Account {
constructor(owner: string, opening: number, private rate: number) {
super(owner, opening);
}
monthlyFee(): number {
return 0;
}
addInterest(): void {
this.deposit(this.balance * this.rate); // protected: accessible in a subclass
}
}
const checking = new CheckingAccount("Ada", 100);
const savings = new SavingsAccount("Ada", 1000, 0.02);
checking.transferTo(savings, 40);
savings.addInterest();
console.log(checking.summary);
console.log(savings.summary);
console.log("history:", savings.history.join(" "));
console.log("fees:", checking.monthlyFee(), savings.monthlyFee(), "accounts:", Account.count);
// @ts-expect-error: abstract classes cannot be instantiated
new Account("Nobody");
// @ts-expect-error: balance is protected
console.log(checking.balance);
Output
ACC-001 Ada: 60.00 ACC-002 Ada: 1060.80 history: +1000 +40 +20.8 fees: 5 0 accounts: 2 60
The last line prints 60 even though balance is protected: @ts-expect-error only silences the compiler, and the access modifiers are gone from the compiled JavaScript. That is exactly why #history exists, and why new Account("Nobody") would also run if the compiler didn't stop you.
Common mistakes
- Assuming
privateis secure. It only exists at compile time; use#fieldwhen it must be hidden at runtime. - Forgetting
super()first in a subclass constructor that uses parameter properties orthis. - Deep inheritance trees. More than two levels usually means you want interfaces and composition.
- Returning a mutable internal array from a getter. Return
readonly string[]or a copy. - Declaring properties without initializers under
strict. Initialise them, mark them optional, or assign in the constructor.
Key points
- Parameter properties (
constructor(public name: string)) remove boilerplate. public,protectedandprivateare compile-time checks;#fieldis private at runtime as well.implementschecks a class against an interface;abstractclasses define partial implementations.- Getters expose computed values; return
readonlytypes to protect internal state.
Further reading: The Handbook: Classes.
Practice questions
What is the difference between `private` and `#private` in TypeScript classes?
Answer
private is only enforced by the compiler and disappears in the JavaScript output, so untyped code can still read the field. #private is JavaScript's own private field syntax, enforced at runtime.
What does `constructor(public readonly owner: string)` do?
Answer
It is a parameter property: it declares a public, read-only property called owner and assigns the constructor argument to it, without writing the field and assignment separately.
Open in the app to mark lessons complete, take notes and track your progress.