ZetCode

TypeScript Classes

last modified October 15, 2026

A class is a template for creating objects that share the same properties and methods. TypeScript classes are JavaScript classes extended with type annotations for properties and methods, access modifiers that control visibility, and checks that a class correctly extends its parent or implements an interface. The compiler verifies that every property is initialized and used with the right type.

Basic Class Syntax

A class declares its properties with types, a constructor that initializes them, and methods.

basic_class.ts
class Person {
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }

    greet(): string {
        return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
    }

    birthday(): void {
        this.age++;
    }
}

const person = new Person("Alice", 30);
console.log(person.greet());

person.birthday();
console.log(person.age);

The properties name and age are declared at the top of the class. The constructor runs when an object is created with new, and this refers to the new object. The birthday method changes the state of the object, which the next line shows.

$ npx tsx basic_class.ts
Hello, my name is Alice and I am 30 years old.
31

Property Initialization

With the strict option, every declared property must be initialized, either in its declaration or in the constructor.

uninitialized.ts
class Account {
    owner: string;
    balance: number = 0;
    createdAt: Date;

    constructor(owner: string) {
        this.owner = owner;
    }
}

The balance property has an initializer and owner is set in the constructor, but createdAt is never assigned. Reading it would return undefined despite its Date type, so the compiler reports it. The check is called strictPropertyInitialization.

$ npx tsc --noEmit
uninitialized.ts(4,5): error TS2564: Property 'createdAt' has no initializer and is not definitely assigned in the constructor.

Parameter Properties

A constructor parameter with an access modifier or readonly declares and initializes a property in one step.

parameter_properties.ts
class Product {
    constructor(
        public readonly id: number,
        public name: string,
        private price: number,
    ) {}

    priceWithTax(rate: number): number {
        return Math.round(this.price * (1 + rate) * 100) / 100;
    }
}

const keyboard = new Product(1, "Keyboard", 45);

console.log(keyboard.id, keyboard.name);
console.log(keyboard.priceWithTax(0.2));

The class is equivalent to declaring three properties and assigning them in the constructor body. The empty constructor body {} is enough. Parameter properties are compact, but they generate code; tools that only strip types, such as Node.js without a build step, do not support them.

$ npx tsx parameter_properties.ts
1 Keyboard
54

Inheritance

The extends keyword creates a class based on another class. The child class inherits the properties and methods of the parent and can add or override them.

inheritance.ts
class Person {
    constructor(public name: string, public age: number) {}

    greet(): string {
        return `Hello, my name is ${this.name}.`;
    }
}

class Employee extends Person {
    constructor(name: string, age: number, public employeeId: number) {
        super(name, age);
    }

    override greet(): string {
        return `${super.greet()} My employee ID is ${this.employeeId}.`;
    }
}

const people: Person[] = [new Person("Alice", 30), new Employee("Bob", 25, 12345)];

for (const p of people) {
    console.log(p.greet());
}

The constructor of Employee must call super before it can use this. The override keyword marks a method that replaces a method of the parent; with the noImplicitOverride option, it is required, and the compiler reports it if the parent has no such method. Inside the method, super.greet() calls the parent version.

The array has the type Person[] but contains an Employee as well. Each object uses its own version of greet; this is called polymorphism.

$ npx tsx inheritance.ts
Hello, my name is Alice.
Hello, my name is Bob. My employee ID is 12345.

Access Modifiers

The public, protected, and private modifiers control where a member can be used. Members are public by default.

access_modifiers.ts
class BankAccount {
    protected balance: number;
    private readonly history: number[] = [];

    constructor(initialBalance: number) {
        this.balance = initialBalance;
    }

    deposit(amount: number): void {
        if (amount <= 0) {
            throw new Error("Deposit must be positive");
        }
        this.balance += amount;
        this.history.push(amount);
    }

    getBalance(): number {
        return this.balance;
    }
}

class SavingsAccount extends BankAccount {
    addInterest(rate: number): void {
        this.balance += this.balance * rate;
    }
}

const account = new SavingsAccount(1000);
account.deposit(500);
account.addInterest(0.02);

console.log(account.getBalance());

A private member, such as history, is accessible only inside the class that declares it. A protected member, such as balance, is also accessible in subclasses, so SavingsAccount can change it. From outside, the balance can only be read and changed through the public methods, which can validate the input.

$ npx tsx access_modifiers.ts
1530
access_error.ts
class BankAccount {
    protected balance = 0;
    private history: number[] = [];
}

const account = new BankAccount();
account.balance = 1000000;
console.log(account.history);

Both accesses from outside the class are rejected. These modifiers are checked only at compile time; in the generated JavaScript, the properties are ordinary properties.

$ npx tsc --noEmit
access_error.ts(7,9): error TS2445: Property 'balance' is protected and only accessible within class 'BankAccount' and its subclasses.
access_error.ts(8,21): error TS2341: Property 'history' is private and only accessible within class 'BankAccount'.

Private Fields

JavaScript has its own private fields, whose names start with #. Unlike the private modifier, they are private also at runtime.

private_fields.ts
class Counter {
    #count = 0;
    private legacy = 0;

    increment(): number {
        this.#count++;
        this.legacy++;
        return this.#count;
    }
}

const counter = new Counter();
counter.increment();
counter.increment();

console.log(counter.increment());
console.log(Object.keys(counter));
console.log(JSON.stringify(counter));

Both fields count the calls, but only legacy is visible as a property of the object: Object.keys and JSON.stringify show it, and plain JavaScript code could change it. The #count field cannot be reached from outside the class at all. Use # fields when the privacy must hold at runtime, for example in libraries.

$ npx tsx private_fields.ts
3
[ 'legacy' ]
{"legacy":3}

Readonly Properties

A readonly property can be assigned only in its declaration or in the constructor.

readonly_properties.ts
class Car {
    readonly make: string;
    readonly model: string;
    readonly createdAt = new Date(2026, 9, 15);
    mileage = 0;

    constructor(make: string, model: string) {
        this.make = make;
        this.model = model;
    }
}

const myCar = new Car("Toyota", "Corolla");
myCar.mileage = 12000;

console.log(`${myCar.make} ${myCar.model}, ${myCar.mileage} km`);
console.log(myCar.createdAt.getFullYear());
// myCar.make = "Honda";  // Error: Cannot assign to 'make' because it is a read-only property

The make, model, and createdAt properties are fixed after construction, while mileage can change. Note that readonly applies to the property, not to the object it holds: the Date object in createdAt could still be modified with its own methods.

$ npx tsx readonly_properties.ts
Toyota Corolla, 12000 km
2026

Static Properties and Methods

Static members belong to the class itself rather than to its instances. They are accessed through the class name.

static_members.ts
class Temperature {
    static readonly ABSOLUTE_ZERO = -273.15;
    static created = 0;

    constructor(public celsius: number) {
        Temperature.created++;
    }

    static fromFahrenheit(fahrenheit: number): Temperature {
        return new Temperature(((fahrenheit - 32) * 5) / 9);
    }
}

const a = new Temperature(25);
const b = Temperature.fromFahrenheit(212);

console.log(a.celsius, b.celsius);
console.log(Temperature.ABSOLUTE_ZERO, Temperature.created);

The ABSOLUTE_ZERO constant and the created counter are shared by all instances. The static method fromFahrenheit is a factory: an alternative way to create objects, here from a different unit. Static members cannot be accessed through an instance, such as a.created.

$ npx tsx static_members.ts
25 100
-273.15 2

Abstract Classes

An abstract class cannot be instantiated. It serves as a base class and can declare abstract methods, which subclasses must implement.

abstract_classes.ts
abstract class Shape {
    constructor(public name: string) {}

    abstract area(): number;

    describe(): string {
        return `${this.name} with area ${this.area().toFixed(2)}`;
    }
}

class Circle extends Shape {
    constructor(private radius: number) {
        super("circle");
    }

    area(): number {
        return Math.PI * this.radius ** 2;
    }
}

class Rectangle extends Shape {
    constructor(private width: number, private height: number) {
        super("rectangle");
    }

    area(): number {
        return this.width * this.height;
    }
}

const shapes: Shape[] = [new Circle(1), new Rectangle(3, 4)];

for (const shape of shapes) {
    console.log(shape.describe());
}

The Shape class provides the shared describe method, which calls the abstract area method. Each subclass supplies its own implementation. A subclass that does not implement area is a compile-time error, and so is new Shape("x").

$ npx tsx abstract_classes.ts
circle with area 3.14
rectangle with area 12.00

Getters and Setters

Accessors, defined with get and set, look like properties from outside but run code when they are read or written.

getters_setters.ts
class Temperature {
    private _celsius = 0;

    get celsius(): number {
        return this._celsius;
    }

    set celsius(value: number) {
        if (value < -273.15) {
            throw new RangeError("Temperature below absolute zero is not possible");
        }
        this._celsius = value;
    }

    get fahrenheit(): number {
        return this._celsius * 1.8 + 32;
    }
}

const temp = new Temperature();
temp.celsius = 25;
console.log(temp.celsius, temp.fahrenheit);

try {
    temp.celsius = -300;
} catch (e) {
    console.log(e instanceof Error ? e.message : e);
}

console.log(temp.celsius);

The setter validates the value before storing it, so the invalid assignment throws and the temperature stays 25. The fahrenheit property has only a getter; it is computed from the stored value, and TypeScript treats it as read-only, so assigning to it would be an error.

$ npx tsx getters_setters.ts
25 77
Temperature below absolute zero is not possible
25

Implementing Interfaces

The implements clause states that a class satisfies an interface. A class can implement several interfaces.

interfaces_with_classes.ts
interface Drivable {
    start(): string;
    stop(): string;
}

interface Refuelable {
    refuel(liters: number): string;
}

class Car implements Drivable, Refuelable {
    private fuel = 10;

    start(): string {
        return "Car started";
    }

    stop(): string {
        return "Car stopped";
    }

    refuel(liters: number): string {
        this.fuel += liters;
        return `Fuel: ${this.fuel} liters`;
    }
}

function testDrive(vehicle: Drivable): void {
    console.log(vehicle.start());
    console.log(vehicle.stop());
}

const myCar = new Car();
testDrive(myCar);
console.log(myCar.refuel(25));

If Car lacked one of the methods or declared it with a different signature, the compiler would report an error at the class. The testDrive function accepts any Drivable and does not need to know about cars. Note that implements only checks the class; it does not add any code or types to it.

$ npx tsx interfaces_with_classes.ts
Car started
Car stopped
Fuel: 35 liters

Classes and Structural Typing

TypeScript compares types by their structure. A class type is satisfied by any object with the same public members, not only by instances of the class.

structural.ts
class Point {
    constructor(public x: number, public y: number) {}
}

function distance(p: Point): number {
    return Math.hypot(p.x, p.y);
}

console.log(distance(new Point(3, 4)));
console.log(distance({ x: 6, y: 8 }));

const plain: Point = { x: 1, y: 1 };
console.log(plain instanceof Point);

The object literal { x: 6, y: 8 } is accepted where a Point is expected, because it has the same properties. However, it is not a real instance: instanceof returns false. Classes with private or # members are an exception; their types can only be satisfied by instances of the class itself.

$ npx tsx structural.ts
5
10
false

Best Practices

Source

TypeScript Handbook: Classes

This tutorial covered classes in TypeScript. They are JavaScript classes with typed properties, access modifiers, and compile-time checks of inheritance and interfaces.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all TypeScript tutorials.