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.
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.
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.
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.
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.
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
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.
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.
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.
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 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.
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.
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.
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
- Initialize All Properties: Keep
strictPropertyInitializationon and give every property a value in its declaration or the constructor. - Keep State Private: Expose behavior through methods and accessors; use
#fields when privacy must hold at runtime. - Mark Overrides: Use the
overridekeyword and enablenoImplicitOverride. - Prefer Composition: Use inheritance for real "is a" relationships and keep hierarchies shallow.
- Program Against Interfaces: Accept interface types in functions, so that any implementation can be passed.
- Use readonly: Mark properties that do not change after construction as
readonly.
Source
This tutorial covered classes in TypeScript. They are JavaScript classes with typed properties, access modifiers, and compile-time checks of inheritance and interfaces.
Author
List all TypeScript tutorials.