Classes and Modules · Lesson 13 of 17
TypeScript Classes and Access Modifiers
JavaScript has had classes since ES2015. TypeScript adds types to their fields and methods, plus access modifiers, readonly, abstract classes and implements. Angular is built around classes (components, services, guards), NestJS on the Node side too – so this is everyday TypeScript for many teams, even if React code leans more on functions.
In this lesson
- Typed fields, constructors and methods
public,private,protectedand JavaScript's#privatereadonly, parameter properties and getters- Abstract classes and
implements
A typed class
class BankAccount {
private balance = 0; // type inferred as number
readonly accountNo: string;
static readonly minDeposit = 100; // belongs to the class, not each object
constructor(accountNo: string, public owner: string) { // 'public owner' declares and assigns a field
this.accountNo = accountNo;
}
deposit(amount: number): this { // returning this allows chaining
if (amount < BankAccount.minDeposit) throw new Error(`minimum deposit is ${BankAccount.minDeposit}`);
this.balance += amount;
return this;
}
get currentBalance(): number { // read like a property, no setter = read-only
return this.balance;
}
}
const acc = new BankAccount("SB-1001", "Asha").deposit(500).deposit(250);
console.log(acc.owner, acc.accountNo, acc.currentBalance);
try {
acc.deposit(50);
} catch (e) {
console.log((e as Error).message);
}
Asha SB-1001 750 minimum deposit is 100
What the modifiers prevent
class BankAccount {
private balance = 0;
readonly accountNo = "SB-1001";
get currentBalance(): number { return this.balance; }
}
const acc = new BankAccount();
acc.balance = 1_000_000;
acc.accountNo = "SB-9999";
acc.currentBalance = 5;
main.ts(7,5): error TS2341: Property 'balance' is private and only accessible within class 'BankAccount'. main.ts(8,5): error TS2540: Cannot assign to 'accountNo' because it is a read-only property. main.ts(9,5): error TS2540: Cannot assign to 'currentBalance' because it is a read-only property.
| Modifier | Accessible from | Enforced |
|---|---|---|
public (default) | anywhere | – |
protected | the class and its subclasses | compile time only |
private | the class only | compile time only |
#field | the class only | at run time too (JavaScript feature) |
private is a compile-time promise
TypeScript's private disappears in the emitted JavaScript, so (acc as any).balance still works at run time. If you need real privacy (a library handling secrets, for example), use JavaScript's #balance syntax, which the engine enforces.
Inheritance and protected
class Employee {
constructor(public readonly name: string, protected baseSalary: number) {}
monthlyPay(): number {
return this.baseSalary / 12;
}
describe(): string {
return `${this.name}: ${this.monthlyPay().toFixed(0)} per month`;
}
}
class Manager extends Employee {
constructor(name: string, baseSalary: number, private teamSize: number) {
super(name, baseSalary); // must call super before using this
}
override monthlyPay(): number { // 'override' catches typos in the method name
return super.monthlyPay() + this.teamSize * 2000; // protected baseSalary used via super
}
}
const staff: Employee[] = [new Employee("Ravi", 600000), new Manager("Meera", 1200000, 5)];
for (const e of staff) console.log(e.describe());
Ravi: 50000 per month Meera: 110000 per month
Abstract classes and implements
interface Notifier {
send(to: string, message: string): string;
}
abstract class BaseNotifier implements Notifier {
private sent = 0;
protected abstract channel: string; // subclasses must provide these
protected abstract format(message: string): string;
send(to: string, message: string): string { // shared logic lives in the base class
this.sent++;
return `[${this.channel} #${this.sent}] to ${to}: ${this.format(message)}`;
}
}
class EmailNotifier extends BaseNotifier {
protected channel = "email";
protected format(message: string): string {
return `Hello, ${message}`;
}
}
class SmsNotifier extends BaseNotifier {
protected channel = "sms";
protected format(message: string): string {
return message.slice(0, 20);
}
}
const notifiers: Notifier[] = [new EmailNotifier(), new SmsNotifier()];
for (const n of notifiers) console.log(n.send("asha", "Your order #4521 has been shipped"));
// new BaseNotifier(); // error: cannot create an instance of an abstract class
[email #1] to asha: Hello, Your order #4521 has been shipped [sms #1] to asha: Your order #4521 has
The notifier hierarchy
- Notifier (interface: the contract)
- BaseNotifier (abstract: shared send logic)
- EmailNotifier
- SmsNotifier
- BaseNotifier (abstract: shared send logic)
Interface or abstract class?
An interface is only a contract and leaves nothing in the JavaScript. An abstract class can hold shared code and state. Code against the interface (Notifier[]), and use an abstract class when several implementations share real logic.
Practice
Try it yourself
Write a
Counterclass with a truly private#count,increment()returningthis, and avaluegetter.Show solution
class Counter { #count = 0; increment(): this { this.#count++; return this; } get value(): number { return this.#count; } } const c = new Counter().increment().increment().increment(); console.log(c.value, Object.keys(c));3 []
Quick check
What does constructor(private name: string) do?
It declares a private field name and assigns the argument to it – a parameter property.
Is TypeScript's private enforced at run time?
No, only at compile time. #field is enforced at run time.
Can you write new on an abstract class?
No – only on concrete subclasses that implement every abstract member.
Summary
- Classes get typed fields, readonly, static members, getters and parameter properties.
- public/protected/private are compile-time checks; #private is enforced by JavaScript.
- override marks overridden methods; abstract classes share code; implements checks a class against an interface.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.