Classes and Decorators
Access modifiers, interfaces implemented by classes, abstract classes, and writing a class decorator.
Classes with access modifiers
TypeScript extends JavaScript classes with access modifiers, checked at compile time (they're erased from the compiled JavaScript output, which has no true private enforcement outside of the newer #field syntax):
class BankAccount {
private balance: number;
public readonly owner: string;
protected accountType: string = "checking";
constructor(owner: string, initialBalance: number) {
this.owner = owner;
this.balance = initialBalance;
}
public deposit(amount: number): void {
if (amount <= 0) throw new Error("Amount must be positive");
this.balance += amount;
}
public getBalance(): number {
return this.balance;
}
}
const account = new BankAccount("Ada", 100);
account.deposit(50);
console.log(account.getBalance()); // 150
// account.balance; // Error: Property 'balance' is private
| Modifier | Visible from |
|---|---|
public (default) |
Anywhere |
private |
Only inside this class |
protected |
This class and its subclasses |
readonly |
Anywhere (like public), but can only be assigned once, at construction |
A shorthand — parameter properties — declares and assigns a constructor parameter as a class field in one step:
class Point {
constructor(
public readonly x: number,
public readonly y: number,
) {}
}
const p = new Point(3, 4);
console.log(p.x, p.y); // 3 4
Interfaces implemented by classes
A class declares it fulfills a contract with implements — the compiler then verifies every required member is actually present:
interface Payable {
calculatePay(): number;
}
class Employee implements Payable {
constructor(
private hoursWorked: number,
private hourlyRate: number,
) {}
calculatePay(): number {
return this.hoursWorked * this.hourlyRate;
}
}
function printPay(payable: Payable) {
console.log(payable.calculatePay());
}
printPay(new Employee(40, 25)); // 1000
A class can implement multiple interfaces (class X implements A, B), and — separately — extend exactly one other class (class X extends Base).
Abstract classes
An abstract class can mix concrete, shared implementation with methods every subclass must implement — and, unlike an interface, it can't be instantiated directly:
abstract class Shape {
abstract area(): number; // must be implemented by every subclass
printArea(): void { // shared, concrete implementation
console.log(`Area: ${this.area()}`);
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
area(): number {
return Math.PI * this.radius ** 2;
}
}
// const shape = new Shape(); // Error: Cannot create an instance of an abstract class
const circle = new Circle(5);
circle.printArea(); // Area: 78.53981633974483
Decorators
A decorator is a function applied to a class (or a class member) with @ syntax, letting you add behavior declaratively — logging, validation, dependency-injection registration — without cluttering the class body itself:
function logged<T extends new (...args: any[]) => object>(target: T, context: ClassDecoratorContext) {
return class extends target {
constructor(...args: any[]) {
console.log(`Creating instance of ${context.name}`);
super(...args);
}
};
}
@logged
class UserService {
constructor(public name: string) {}
}
new UserService("Ada");
// Creating instance of UserService
A note on decorator versions. TypeScript 5.0+ implements the modern, TC39 Stage 3 ECMAScript decorators proposal shown above — it works with "target": "ES2022" or later and needs no special compiler flag. You'll still frequently encounter an older, different decorator syntax (used by frameworks like Angular and NestJS) that requires "experimentalDecorators": true and often reflect-metadata — that legacy form predates the official spec and is not directly compatible with the standard one. When starting a new project, prefer the standard (no-flag) decorators unless a specific framework's documentation tells you to enable experimentalDecorators.
Common mistakes
- Assuming
privateprovides real runtime encapsulation — it's a compile-time-only check, erased in the compiled JavaScript. For genuine runtime privacy, use JavaScript's native#fieldNameprivate field syntax instead. - Mixing the legacy
experimentalDecoratorssyntax with the modern standard decorators in the same project — they have different underlying semantics and generally aren't interchangeable. - Trying to
newan abstract class directly, or forgetting to callsuper()in a subclass constructor before accessingthis.
Interview questions
Q: What's the difference between private in TypeScript and JavaScript's #field syntax?
TypeScript's private is a compile-time-only annotation — it disappears entirely from the compiled JavaScript, so code that bypasses the type checker (or plain JS consuming your compiled output) can still access it. JavaScript's native #field is enforced by the runtime itself — attempting to access #field from outside the class is a real runtime error, not just a type error.
Q: When would you use an abstract class instead of an interface in TypeScript?
When you want subclasses to share actual concrete method implementations and possibly shared state, not just a method signature contract. An interface only describes shape — it can never carry an implementation; an abstract class can provide default behavior (like printArea() above) while still forcing subclasses to implement the parts that must vary (area()).