Skip to content

Class

Class Common Syntax

The class keyword is used in TypeScript to define a class. Below, you can see an example:

class Person {
private name: string;
private age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
public sayHi(): void {
console.log(
`Hello, my name is ${this.name} and I am ${this.age} years old.`
);
}
}

The class keyword is used to define a class named “Person”.

The class has two private properties: name of type string and age of type number.

The constructor is defined using the constructor keyword. It takes name and age as parameters and assigns them to the corresponding properties.

The class has a public method named sayHi that logs a greeting message.

To create an instance of a class in TypeScript, you can use the new keyword followed by the class name, followed by parentheses (). For instance:

const myObject = new Person('John Doe', 25);
myObject.sayHi(); // Output: Hello, my name is John Doe and I am 25 years old.

Constructor

Constructors are special methods within a class that are used to initialize the object’s properties when an instance of the class is created.

class Person {
public name: string;
public age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
sayHello() {
console.log(
`Hello, my name is ${this.name} and I'm ${this.age} years old.`
);
}
}
const john = new Person('Simon', 17);
john.sayHello();

It is possible to overload a constructor using the following syntax:

type Sex = 'm' | 'f';
class Person {
name: string;
age: number;
sex: Sex;
constructor(name: string, age: number, sex?: Sex);
constructor(name: string, age: number, sex: Sex) {
this.name = name;
this.age = age;
this.sex = sex ?? 'm';
}
}
const p1 = new Person('Simon', 17);
const p2 = new Person('Alice', 22, 'f');

In TypeScript, it is possible to define multiple constructor overloads, but you can have only one implementation that must be compatible with all the overloads, this can be achieved by using an optional parameter.

class Person {
name: string;
age: number;
constructor();
constructor(name: string);
constructor(name: string, age: number);
constructor(name?: string, age?: number) {
this.name = name ?? 'Unknown';
this.age = age ?? 0;
}
displayInfo() {
console.log(`Name: ${this.name}, Age: ${this.age}`);
}
}
const person1 = new Person();
person1.displayInfo(); // Name: Unknown, Age: 0
const person2 = new Person('John');
person2.displayInfo(); // Name: John, Age: 0
const person3 = new Person('Jane', 25);
person3.displayInfo(); // Name: Jane, Age: 25

Private and Protected Constructors

In TypeScript, constructors can be marked as private or protected, which restricts their accessibility and usage.

Private Constructors: Can be called only within the class itself. Private constructors are often used in scenarios where you want to enforce a singleton pattern or restrict the creation of instances to a factory method within the class

Protected Constructors: Protected constructors are useful when you want to create a base class that should not be instantiated directly but can be extended by subclasses.

class BaseClass {
protected constructor() {}
}
class DerivedClass extends BaseClass {
private value: number;
constructor(value: number) {
super();
this.value = value;
}
}
// Attempting to instantiate the base class directly will result in an error
// const baseObj = new BaseClass(); // Error: Constructor of class 'BaseClass' is protected.
// Create an instance of the derived class
const derivedObj = new DerivedClass(10);

Access Modifiers

Access Modifiers private, protected, and public are used to control the visibility and accessibility of class members, such as properties and methods, in TypeScript classes. These modifiers are essential for enforcing encapsulation and establishing boundaries for accessing and modifying the internal state of a class.

The private modifier restricts access to the class member only within the containing class.

The protected modifier allows access to the class member within the containing class and its derived classes.

The public modifier provides unrestricted access to the class member, allowing it to be accessed from anywhere.”

Get and Set

Getters and setters are special methods that allow you to define custom access and modification behavior for class properties. They enable you to encapsulate the internal state of an object and provide additional logic when getting or setting the values of properties. In TypeScript, getters and setters are defined using the get and set keywords respectively. Here’s an example:

class MyClass {
private _myProperty: string;
constructor(value: string) {
this._myProperty = value;
}
get myProperty(): string {
return this._myProperty;
}
set myProperty(value: string) {
this._myProperty = value;
}
}

Auto-Accessors in Classes

TypeScript version 4.9 adds support for auto-accessors, a forthcoming ECMAScript feature. They resemble class properties but are declared with the “accessor” keyword.

class Animal {
accessor name: string;
constructor(name: string) {
this.name = name;
}
}

Auto-accessors are “de-sugared” into private get and set accessors, operating on an inaccessible property.

class Animal {
#__name: string;
get name() {
return this.#__name;
}
set name(value: string) {
this.#__name = name;
}
constructor(name: string) {
this.name = name;
}
}

this

In TypeScript, the this keyword refers to the current instance of a class within its methods or constructors. It allows you to access and modify the properties and methods of the class from within its own scope. It provides a way to access and manipulate the internal state of an object within its own methods.

class Person {
private name: string;
constructor(name: string) {
this.name = name;
}
public introduce(): void {
console.log(`Hello, my name is ${this.name}.`);
}
}
const person1 = new Person('Alice');
person1.introduce(); // Hello, my name is Alice.

Parameter Properties

Parameter properties allow you to declare and initialize class properties directly within the constructor parameters avoiding boilerplate code, example:

class Person {
constructor(
private name: string,
public age: number
) {
// The "private" and "public" keywords in the constructor
// automatically declare and initialize the corresponding class properties.
}
public introduce(): void {
console.log(
`Hello, my name is ${this.name} and I am ${this.age} years old.`
);
}
}
const person = new Person('Alice', 25);
person.introduce();

Abstract Classes

Abstract Classes are used in TypeScript mainly for inheritance, they provide a way to define common properties and methods that can be inherited by subclasses. This is useful when you want to define common behavior and enforce that subclasses implement certain methods. They provide a way to create a hierarchy of classes where the abstract base class provides a shared interface and common functionality for the subclasses.

abstract class Animal {
protected name: string;
constructor(name: string) {
this.name = name;
}
abstract makeSound(): void;
}
class Cat extends Animal {
makeSound(): void {
console.log(`${this.name} meows.`);
}
}
const cat = new Cat('Whiskers');
cat.makeSound(); // Output: Whiskers meows.

With Generics

Classes with generics allow you to define reusable classes which can work with different types.

class Container<T> {
private item: T;
constructor(item: T) {
this.item = item;
}
getItem(): T {
return this.item;
}
setItem(item: T): void {
this.item = item;
}
}
const container1 = new Container<number>(42);
console.log(container1.getItem()); // 42
const container2 = new Container<string>('Hello');
container2.setItem('World');
console.log(container2.getItem()); // World

Decorators

Decorators provide a mechanism to add metadata, modify behavior, validate, or extend the functionality of the target element. They are functions that execute at runtime. Multiple decorators can be applied to a declaration.

Decorators are experimental features, and the following examples are only compatible with TypeScript version 5 or above using ES6.

For TypeScript versions prior to 5, they should be enabled using the experimentalDecorators property in your tsconfig.json or by using --experimentalDecorators in your command line (but the following example won’t work).

Some of the common use cases for decorators include:

  • Watching property changes.
  • Watching method calls.
  • Adding extra properties or methods.
  • Runtime validation.
  • Automatic serialization and deserialization.
  • Logging.
  • Authorization and authentication.
  • Error guarding.

Note: Decorators for version 5 do not allow decorating parameters.

Types of decorators:

Class Decorators

Class Decorators are useful for extending an existing class, such as adding properties or methods, or collecting instances of a class. In the following example, we add a toString method that converts the class into a string representation.

type Constructor<T = {}> = new (...args: any[]) => T;
function toString<Class extends Constructor>(
Value: Class,
context: ClassDecoratorContext<Class>
) {
return class extends Value {
constructor(...args: any[]) {
super(...args);
console.log(JSON.stringify(this));
console.log(JSON.stringify(context));
}
};
}
@toString
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet() {
return 'Hello, ' + this.name;
}
}
const person = new Person('Simon');
/* Logs:
{"name":"Simon"}
{"kind":"class","name":"Person"}
*/

Property Decorator

Property decorators are useful for modifying the behavior of a property, such as changing the initialization values. In the following code, we have a script that sets a property to always be in uppercase:

function upperCase<T>(
target: undefined,
context: ClassFieldDecoratorContext<T, string>
) {
return function (this: T, value: string) {
return value.toUpperCase();
};
}
class MyClass {
@upperCase
prop1 = 'hello!';
}
console.log(new MyClass().prop1); // Logs: HELLO!

Method Decorator

Method decorators allow you to change or enhance the behavior of methods. Below is an example of a simple logger:

function log<This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<
This,
(this: This, ...args: Args) => Return
>
) {
const methodName = String(context.name);
function replacementMethod(this: This, ...args: Args): Return {
console.log(`LOG: Entering method '${methodName}'.`);
const result = target.call(this, ...args);
console.log(`LOG: Exiting method '${methodName}'.`);
return result;
}
return replacementMethod;
}
class MyClass {
@log
sayHello() {
console.log('Hello!');
}
}
new MyClass().sayHello();

It logs:

Terminal window
LOG: Entering method 'sayHello'.
Hello!
LOG: Exiting method 'sayHello'.

Getter and Setter Decorators

Getter and setter decorators allow you to change or enhance the behavior of class accessors. They are useful, for instance, for validating property assignments. Here’s a simple example for a getter decorator:

function range<This, Return extends number>(min: number, max: number) {
return function (
target: (this: This) => Return,
context: ClassGetterDecoratorContext<This, Return>
) {
return function (this: This): Return {
const value = target.call(this);
if (value < min || value > max) {
throw 'Invalid';
}
Object.defineProperty(this, context.name, {
value,
enumerable: true,
});
return value;
};
};
}
class MyClass {
private _value = 0;
constructor(value: number) {
this._value = value;
}
@range(1, 100)
get getValue(): number {
return this._value;
}
}
const obj = new MyClass(10);
console.log(obj.getValue); // Valid: 10
const obj2 = new MyClass(999);
console.log(obj2.getValue); // Throw: Invalid!

Decorator Metadata

Decorator Metadata simplifies the process for decorators to apply and utilize metadata in any class. They can access a new metadata property on the context object, which can serve as a key for both primitives and objects. Metadata information can be accessed on the class via Symbol.metadata.

Metadata can be used for various purposes, such as debugging, serialization, or dependency injection with decorators.

//@ts-ignore
Symbol.metadata ??= Symbol('Symbol.metadata'); // Simple polify
type Context =
| ClassFieldDecoratorContext
| ClassAccessorDecoratorContext
| ClassMethodDecoratorContext; // Context contains property metadata: DecoratorMetadata
function setMetadata(_target: any, context: Context) {
// Set the metadata object with a primitive value
context.metadata[context.name] = true;
}
class MyClass {
@setMetadata
a = 123;
@setMetadata
accessor b = 'b';
@setMetadata
fn() {}
}
const metadata = MyClass[Symbol.metadata]; // Get metadata information
console.log(JSON.stringify(metadata)); // {"bar":true,"baz":true,"foo":true}

Inheritance

Inheritance refers to the mechanism by which a class can inherit properties and methods from another class, known as the base class or superclass. The derived class, also called the child class or subclass, can extend and specialize the functionality of the base class by adding new properties and methods or overriding existing ones.

class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
speak(): void {
console.log('The animal makes a sound');
}
}
class Dog extends Animal {
breed: string;
constructor(name: string, breed: string) {
super(name);
this.breed = breed;
}
speak(): void {
console.log('Woof! Woof!');
}
}
// Create an instance of the base class
const animal = new Animal('Generic Animal');
animal.speak(); // The animal makes a sound
// Create an instance of the derived class
const dog = new Dog('Max', 'Labrador');
dog.speak(); // Woof! Woof!"

TypeScript does not support multiple inheritance in the traditional sense and instead allows inheritance from a single base class. TypeScript supports multiple interfaces. An interface can define a contract for the structure of an object, and a class can implement multiple interfaces. This allows a class to inherit behavior and structure from multiple sources.

interface Flyable {
fly(): void;
}
interface Swimmable {
swim(): void;
}
class FlyingFish implements Flyable, Swimmable {
fly() {
console.log('Flying...');
}
swim() {
console.log('Swimming...');
}
}
const flyingFish = new FlyingFish();
flyingFish.fly();
flyingFish.swim();

The class keyword in TypeScript, similar to JavaScript, is often referred to as syntactic sugar. It was introduced in ECMAScript 2015 (ES6) to offer a more familiar syntax for creating and working with objects in a class-based manner. However, it’s important to note that TypeScript, being a superset of JavaScript, ultimately compiles down to JavaScript, which remains prototype-based at its core.

Statics

TypeScript has static members. To access the static members of a class, you can use the class name followed by a dot, without the need to create an object.

class OfficeWorker {
static memberCount: number = 0;
constructor(private name: string) {
OfficeWorker.memberCount++;
}
}
const w1 = new OfficeWorker('James');
const w2 = new OfficeWorker('Simon');
const total = OfficeWorker.memberCount;
console.log(total); // 2

Property initialization

There are several ways how you can initialize properties for a class in TypeScript:

Inline:

In the following example these initial values will be used when an instance of the class is created.

class MyClass {
property1: string = 'default value';
property2: number = 42;
}

In the constructor:

class MyClass {
property1: string;
property2: number;
constructor() {
this.property1 = 'default value';
this.property2 = 42;
}
}

Using constructor parameters:

class MyClass {
constructor(
private property1: string = 'default value',
public property2: number = 42
) {
// There is no need to assign the values to the properties explicitly.
}
log() {
console.log(this.property2);
}
}
const x = new MyClass();
x.log();

Method overloading

Method overloading allows a class to have multiple methods with the same name but different parameter types or a different number of parameters. This allows us to call a method in different ways based on the arguments passed.

class MyClass {
add(a: number, b: number): number; // Overload signature 1
add(a: string, b: string): string; // Overload signature 2
add(a: number | string, b: number | string): number | string {
if (typeof a === 'number' && typeof b === 'number') {
return a + b;
}
if (typeof a === 'string' && typeof b === 'string') {
return a.concat(b);
}
throw new Error('Invalid arguments');
}
}
const r = new MyClass();
console.log(r.add(10, 5)); // Logs 15