Classes & Access Modifiers
JavaScript classes work the same in TypeScript, but TypeScript adds types for fields, parameters and return values, plus access modifiers that control who may use each member. It also checks that every field gets a value, that subclasses override methods correctly and that a class really provides everything an interface promises.
This lesson assumes you know JavaScript classes. If not, read Classes in the JavaScript tutorial first. Here you will add types, use public, private, protected and readonly, compare TypeScript's private with JavaScript's #private, and write interfaces, abstract classes and overrides.
Typed fields and constructors
In TypeScript, class fields are declared in the class body with a type. The constructor fills them in.
class Ticket {
readonly pnr: string;
passenger: string;
seat: number | null = null;
#fare: number;
constructor(pnr: string, passenger: string, fare: number) {
this.pnr = pnr;
this.passenger = passenger;
this.#fare = fare;
}
allot(seat: number): void {
this.seat = seat;
}
get fare(): number {
return this.#fare;
}
summary(): string {
const seatText = this.seat === null ? "RAC" : `seat ${this.seat}`;
return `${this.pnr}: ${this.passenger}, ${seatText}, ₹${this.fare}`;
}
}
const t = new Ticket("4521890", "Ananya", 1450);
console.log(t.summary());
t.allot(23);
console.log(t.summary());
console.log(Object.keys(t));4521890: Ananya, RAC, ₹1450 4521890: Ananya, seat 23, ₹1450 [ 'pnr', 'passenger', 'seat' ]
readonly pnr can be set in the constructor but never changed afterwards. #fare is a JavaScript private field: it is invisible outside the class and does not appear in Object.keys(). The getter fare gives read-only access to it.
With strict on, TypeScript checks that every field is definitely given a value, either where it is declared or in the constructor. It also enforces the modifiers:
class Wallet {
owner: string;
private balance = 0;
protected history: string[] = [];
readonly id: number;
constructor(owner: string, id: number) {
this.owner = owner;
this.id = id;
}
}
class Profile {
name: string;
}
const w = new Wallet("Isha", 1);
w.balance = 5000;
w.history.push("hack");
w.id = 2;app.ts(14,3): error TS2564: Property 'name' has no initializer and is not definitely assigned in the constructor. app.ts(18,3): error TS2341: Property 'balance' is private and only accessible within class 'Wallet'. app.ts(19,3): error TS2445: Property 'history' is protected and only accessible within class 'Wallet' and its subclasses. app.ts(20,3): error TS2540: Cannot assign to 'id' because it is a read-only property.
Access modifiers
TypeScript has three access modifiers:
| Modifier | Who can use the member |
|---|---|
public (the default) | Everyone |
protected | The class and its subclasses |
private | Only the class itself |
There is an important difference between TypeScript's private keyword and JavaScript's # private fields. The keyword is checked only by the compiler and is erased; at run time the property is a normal, visible property. A # field is private in the running JavaScript too.
class Account {
private pin = "4321"; // TypeScript-only privacy
#otp = "889102"; // real JavaScript privacy
check(pin: string): boolean {
return pin === this.pin;
}
}
const acc = new Account();
console.log(acc.check("4321"));
console.log(JSON.stringify(acc));
console.log(Object.keys(acc));true
{"pin":"4321"}
[ 'pin' ]The private pin still appears in JSON.stringify and Object.keys, and plain JavaScript code (or acc["pin"]) can read it. The #otp field appears nowhere. Use # fields for data that must stay hidden, and the private keyword when compile-time protection is enough or when you work in code that already uses it.
Interfaces, abstract classes and override
A class can promise to match an interface with implements. TypeScript then checks that every required member exists with the right type. An abstract class is a base class that cannot be created with new and can declare abstract methods that each subclass must write.
interface Payable {
amountDue(): number;
}
abstract class Employee implements Payable {
readonly name: string;
protected baseSalary = 30000;
constructor(name: string) {
this.name = name;
}
abstract role(): string;
amountDue(): number {
return this.baseSalary;
}
describe(): string {
return `${this.name} (${this.role()}): ₹${this.amountDue()}`;
}
}
class Developer extends Employee {
role(): string {
return "Developer";
}
override amountDue(): number {
return this.baseSalary + 25000;
}
}
class Intern extends Employee {
role(): string {
return "Intern";
}
override amountDue(): number {
return Math.round(this.baseSalary * 0.4);
}
}
const team: Employee[] = [new Developer("Kabir"), new Intern("Priya")];
for (const person of team) {
console.log(person.describe());
}
const total = team.reduce((sum, p) => sum + p.amountDue(), 0);
console.log(`Payroll: ₹${total}`);Kabir (Developer): ₹55000 Priya (Intern): ₹12000 Payroll: ₹67000
implements Payablemakes the compiler check thatamountDue()exists and returns a number.abstract role()has no body;new Employee("x")would be an error, and any subclass that forgetsrole()is an error too.protected baseSalaryis usable inDeveloperandInternbut not from outside.overridemarks a method that replaces a parent method. With thenoImplicitOverrideoption, TypeScript requires it, and it reports an error if the parent method is renamed, so overrides never silently break.
Parameter properties: the older shortcut
TypeScript has a shortcut that declares and assigns a field in one go, by putting a modifier on a constructor parameter. These are called parameter properties, and you will see them in many existing projects, especially Angular:
// Older style: parameter properties (not erasable)
class Student {
constructor(public name: string, private rollNo: number) {}
}
// Modern, erasable equivalent
class StudentModern {
name: string;
#rollNo: number;
constructor(name: string, rollNo: number) {
this.name = name;
this.#rollNo = rollNo;
}
}Parameter properties generate assignment code, so they are not erasable syntax: Node.js type stripping rejects them, and the erasableSyntaxOnly option reports them (see Enums & Erasable Syntax). For new code, prefer declaring fields explicitly as in StudentModern. It is a few lines longer but works with every tool.
Common mistakes
- Thinking
privatehides data at run time. It does not; use#fieldsfor real privacy. - Forgetting to initialise a field. Give it a default, set it in the constructor, or make it optional with
?. - Calling an abstract class with
new. Create one of its subclasses instead. - Overriding by accident. Turn on
noImplicitOverrideso every override is marked and checked. - Using parameter properties in Node.js code. They fail with type stripping; declare fields explicitly.
Key points
- Declare class fields with types; strict mode checks that each field gets a value.
public,protectedandprivatecontrol access at compile time;#privatefields are private at run time too.readonlyfields can be assigned only in their declaration or the constructor.implementschecks a class against an interface;abstractclasses define members subclasses must provide.overridemarks replaced methods; parameter properties are an older, non-erasable shortcut.
Exercise
Create an interface Shape with name: string and area(): number. Write an abstract class BaseShape that implements it and adds a method report(): string returning text like Circle: 78.54 sq cm. Create Circle (radius) and Rectangle (width, height) subclasses with explicitly declared fields. Put one of each in an array and log every report.
Show solution
interface Shape {
name: string;
area(): number;
}
abstract class BaseShape implements Shape {
abstract name: string;
abstract area(): number;
report(): string {
return `${this.name}: ${this.area().toFixed(2)} sq cm`;
}
}
class Circle extends BaseShape {
name = "Circle";
#radius: number;
constructor(radius: number) {
super();
this.#radius = radius;
}
area(): number {
return Math.PI * this.#radius ** 2;
}
}
class Rectangle extends BaseShape {
name = "Rectangle";
width: number;
height: number;
constructor(width: number, height: number) {
super();
this.width = width;
this.height = height;
}
area(): number {
return this.width * this.height;
}
}
const shapes: Shape[] = [new Circle(5), new Rectangle(4, 6.5)];
for (const s of shapes) {
if (s instanceof BaseShape) console.log(s.report());
}
// Circle: 78.54 sq cm
// Rectangle: 26.00 sq cmBaseShape declares the members every shape must have, and each subclass declares its own fields explicitly, so the code is erasable and works with Node.js type stripping.
Quick Quiz
1. Where can a protected member be used?
protected allows access from subclasses but not from outside code.
2. What is the run-time difference between private x and #x?
The private keyword is erased, while # fields are enforced by JavaScript.
3. What does implements Payable do?
implements is a compile-time check; it adds no code.
4. Why do parameter properties like constructor(public name: string) fail with Node.js type stripping?
Parameter properties need a code transformation, which type stripping does not do.
Learn TypeScript with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
