On this page
Classes and decorators
Classes in TypeScript
TypeScript builds on JavaScript classes by adding a type system layer: access modifiers, readonly properties, parameter properties, abstract classes, and static members. All of this compiles away to standard JavaScript — classes are not a TypeScript-only concept at runtime.
Access modifiers
TypeScript provides three access modifiers that control where a property or method can be accessed:
| Modifier | Accessible in | Compile-time only? |
|---|---|---|
public |
Everywhere (default) | No |
protected |
Class and subclasses | Yes |
private |
Class only | Yes |
Additionally, JavaScript's #name syntax provides hard private fields enforced at runtime:
class Counter {
#count = 0; // true runtime privacy
increment(): void { this.#count++; }
get value(): number { return this.#count; }
}
const c = new Counter();
c.increment();
console.log(c.value); // 1
// (c as any).#count // SyntaxError at runtime — not just a type errorParameter properties
TypeScript's parameter properties let you declare and initialize class members directly in the constructor signature, eliminating boilerplate:
// Without parameter properties
class UserVerbose {
public name: string;
private email: string;
readonly id: number;
constructor(name: string, email: string, id: number) {
this.name = name;
this.email = email;
this.id = id;
}
}
// With parameter properties — identical result
class User {
constructor(
public name: string,
private email: string,
readonly id: number
) {}
}Both classes are equivalent. The shorthand form is idiomatic TypeScript.
Readonly properties
A readonly property can only be assigned during declaration or in the constructor. Attempts to reassign it elsewhere are compile errors:
class Config {
readonly maxRetries: number;
readonly baseUrl: string;
constructor(maxRetries: number, baseUrl: string) {
this.maxRetries = maxRetries;
this.baseUrl = baseUrl;
}
}
const config = new Config(3, "https://api.example.com");
// config.maxRetries = 5; // Error: cannot assign to 'maxRetries' — it is a read-only propertyStatic members
Static properties and methods belong to the class itself, not to instances. They are useful for factory methods, counters, and shared utilities:
class IdGenerator {
private static nextId = 1;
static generate(): number {
return IdGenerator.nextId++;
}
static reset(): void {
IdGenerator.nextId = 1;
}
}
console.log(IdGenerator.generate()); // 1
console.log(IdGenerator.generate()); // 2Abstract classes
An abstract class defines a contract that subclasses must fulfill. You cannot create an instance of an abstract class directly — it exists only to be extended:
abstract class Transport {
abstract send(payload: string): Promise<void>;
async sendJson(data: unknown): Promise<void> {
await this.send(JSON.stringify(data));
}
}
class HttpTransport extends Transport {
constructor(private url: string) { super(); }
async send(payload: string): Promise<void> {
await fetch(this.url, {
method: "POST",
body: payload,
headers: { "Content-Type": "application/json" },
});
}
}
class LogTransport extends Transport {
async send(payload: string): Promise<void> {
console.log("[LogTransport]", payload);
}
}Both HttpTransport and LogTransport inherit the concrete sendJson method for free and only need to implement send.
The `implements` keyword
A class can implement one or more interfaces. This enforces the contract at compile time without affecting the runtime prototype chain:
interface Serializable {
serialize(): string;
deserialize(raw: string): void;
}
interface Loggable {
log(): void;
}
class Session implements Serializable, Loggable {
private data: Record<string, string> = {};
serialize(): string {
return JSON.stringify(this.data);
}
deserialize(raw: string): void {
this.data = JSON.parse(raw) as Record<string, string>;
}
log(): void {
console.log("Session:", this.data);
}
}TC39 decorators
Decorators are a Stage 3 TC39 proposal, natively supported in TypeScript 5.0+ without any compiler flags. A decorator is a function that receives the target it decorates (a class, method, property, or accessor) along with a context object describing it.
Method decorator
function memoize(
target: unknown,
context: ClassMethodDecoratorContext
): unknown {
const cache = new Map<string, unknown>();
return function (this: unknown, ...args: unknown[]): unknown {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = (target as (...a: unknown[]) => unknown).apply(this, args);
cache.set(key, result);
return result;
};
}
class MathUtils {
@memoize
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}Class decorator
function singleton<T extends new (...args: unknown[]) => unknown>(
target: T,
context: ClassDecoratorContext
): T {
let instance: InstanceType<T> | undefined;
return class extends target {
constructor(...args: unknown[]) {
if (instance) return instance;
super(...args);
instance = this as InstanceType<T>;
}
} as T;
}
@singleton
class AppConfig {
theme = "dark";
}
const a = new AppConfig();
const b = new AppConfig();
console.log(a === b); // trueProperty decorator
function validate(
target: undefined,
context: ClassFieldDecoratorContext
): (value: unknown) => unknown {
return function (initialValue: unknown): unknown {
if (typeof initialValue !== "string" || initialValue.trim() === "") {
throw new Error(`Field "${String(context.name)}" must be a non-empty string`);
}
return initialValue;
};
}
class Article {
@validate
title = "My first article";
}Practice
- Access modifiers: Build a
Queue<T>class with a private#items: T[]. Exposeenqueue(item: T),dequeue(): T | undefined, and a readonlysizegetter. Verify that attempting to access#itemsdirectly from outside the class fails. - Abstract class: Define an abstract
Validator<T>class with an abstractvalidate(value: T): string[]method and a concreteisValid(value: T): booleanmethod that returnstruewhenvalidatereturns an empty array. ImplementEmailValidatorandPhoneValidatorsubclasses. - Implements multiple interfaces: Create
PrintableandExportableinterfaces. Build aReportclass that implements both, storing an array of strings and implementingprint(): voidandexport(): string. - Method decorator: Write a
deprecated(message: string)decorator factory that logs a warning to the console the first time the decorated method is called. Apply it to a method and verify the warning appears exactly once.
Next lesson: modules and namespaces — organizing TypeScript code across files and packages.
class BankAccount {
// public — accessible everywhere (default)
readonly id: string;
// private — only accessible inside BankAccount
#balance: number; // TC39 private field (hard private)
// protected — accessible in BankAccount and subclasses
protected owner: string;
// Parameter properties — shorthand for declare + assign
constructor(
public readonly bank: string,
owner: string,
initialBalance: number
) {
this.id = crypto.randomUUID();
this.owner = owner;
this.#balance = initialBalance;
}
deposit(amount: number): void {
if (amount <= 0) throw new RangeError("Amount must be positive");
this.#balance += amount;
}
get balance(): number {
return this.#balance;
}
}
class SavingsAccount extends BankAccount {
#interestRate: number;
constructor(bank: string, owner: string, balance: number, rate: number) {
super(bank, owner, balance);
this.#interestRate = rate;
}
applyInterest(): void {
const interest = this.balance * this.#interestRate;
this.deposit(interest);
// this.#balance — Error: #balance is private to BankAccount
// this.owner — OK: protected, accessible in subclass
}
}
// Abstract class — cannot be instantiated directly
abstract class Shape {
abstract area(): number;
abstract perimeter(): number;
describe(): string {
return `Area: ${this.area().toFixed(2)}, Perimeter: ${this.perimeter().toFixed(2)}`;
}
}
class Circle extends Shape {
constructor(private radius: number) { super(); }
area(): number { return Math.PI * this.radius ** 2; }
perimeter(): number { return 2 * Math.PI * this.radius; }
}
class Rectangle extends Shape {
constructor(private w: number, private h: number) { super(); }
area(): number { return this.w * this.h; }
perimeter(): number { return 2 * (this.w + this.h); }
}
// TC39 Stage 3 decorators (TypeScript 5.0+ with experimentalDecorators: false)
function log(target: unknown, context: ClassMethodDecoratorContext): unknown {
const methodName = String(context.name);
return function(this: unknown, ...args: unknown[]): unknown {
console.log(`[${methodName}] called with`, args);
const result = (target as (...a: unknown[]) => unknown).apply(this, args);
console.log(`[${methodName}] returned`, result);
return result;
};
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
@log
multiply(a: number, b: number): number {
return a * b;
}
}
const calc = new Calculator();
calc.add(3, 4); // logs: [add] called with [3, 4] ... [add] returned 7
calc.multiply(3, 4); // logs: [multiply] called with [3, 4] ... returned 12
Sign in to track your progress