Classes page

Learn how to use classes and inheritance in TypeScript, understand the constructor method, and use the public, private, protected, and readonly modifiers.

Overview

In this section, you will:

  • Use classes in TypeScript.
  • Use the constructor method.
  • Manage this in classes.
  • Implement inheritance in classes.
  • Use the public, private, protected, and readonly modifiers.

Objective 1: Create a class

Classes in JavaScript

In ECMAScript 2015, classes are available as syntactic sugar over the existing prototype-based constructor functions. A class may look like:

class ParkEmployee {
  constructor(name) {
    this.name = name;
  }
  sayHi() {
    console.info("Hi, my name is " + this.name);
  }
}

const raptorGuy = new ParkEmployee("Owen");
raptorGuy.sayHi();

The ParkEmployee class can be instantiated with a name field, and contains a sayHi() method.

So when raptorGuy.sayHi() is called, since ParkEmployee was instantiated with new ParkEmployee("Owen") it logs Hi, my name is Owen.

For more information on JavaScript classes, check out the Advanced JavaScript Classes Training. The following sections will cover features TypeScript adds to JavaScript.

Classes in TypeScript

Classes in TypeScript look like classes in JavaScript; however, there are additional features that add type safety.

In the following TypeScript class example, the name field is defined on line 2. We’ll look at setting the name via the constructor next.

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

  sayHi() {
    console.info(`Hi, my name is ${this.name}`);
  }
}

const raptorGuy = new ParkEmployee("Owen");
raptorGuy.sayHi();

The functionality is identical to the TypeScript class’s Javascript counterpart, however, the name field has been given a specific type: string. This ensures that the constructor will only accept a string value as input.

Constructor

The constructor method is how to initialize a new object with fields. The constructor is called when we instantiate a new object from calling a class with the new keyword — it constructs and returns a new object for us with properties we gave it.

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

let dino = new Dinosaur("Billy");
console.info(dino.name);
// Logs "Billy"

When declaring fields, it’s also possible to instantiate a value on them.

class Dinosaur {
  name: string;
  age = 0;
  constructor(name: string) {
    this.name = name;
  }
}

let dino = new Dinosaur("Billy");
console.info(dino.age);
// Logs "0"

Using the constructor to set public fields is quite a common pattern, which is why TypeScript also provides a shorthand.

class Dinosaur {
  constructor(public name: string) {}
}
let dino = new Dinosaur("Billy");
console.info(dino.name);
// Logs "Billy"

Note: We will see how to create private fields later.

Static fields

When you need a property to be shared across multiple instances, you can use a static property. These are shared by all instances of the class as well as inheriting classes. Both fields and methods on a class can be static. Each instance accesses the static value through prepending the name of the class.

This example shows the use of a static property cageInstances to count the number of instances of DinoCage:

class DinoCage {
  static cageInstances = 0;
  constructor() {
    DinoCage.cageInstances++;
  }
}

var paddock1 = new DinoCage();
var paddock2 = new DinoCage();
console.info(DinoCage.cageInstances);
// Logs "2"

This and arrow => functions

If you’re familiar with ES6, you may know that using the arrow => captures the context of this where it’s used. The functionality is the same in TypeScript.

class DinoBuilder {
  dinoName = "Trex";
  yawn() {
    setTimeout(function () {
      console.info(`${this.dinoName} yawned.`);
    }, 50);
  }
}

const dino = new DinoBuilder();
dino.yawn();
// Logs “undefined yawned”

For example, in the above code block there is no =>, thus the yawn() method can’t access the class DinoBuilder’s dinoName property; there’s no way it reaches DinoBuilder’s this context. As a result, when the instance of dino invokes its yawn() method, what is logged is undefined yawned.

Let’s look at an example with an arrow function:

class DinoBuilder {
  dinoName = "Trex";
  yawn() {
    setTimeout(() => {
      console.info(`${this.dinoName} yawned.`);
    }, 50);
  }
}

const dino = new DinoBuilder();
dino.yawn();
// Logs “Trex yawned”

In the above example setTimeout instead uses =>. Now the yawn() method can access this context of the DinoBuilder class, so the invoked yawn() method will now log Trex yawned.

class DinoBuilder {
  dinoName = "Trex";
  roar() {
    console.info(`${this.dinoName} roared.`);
  }
}

const dino = new DinoBuilder();

setTimeout(dino.roar, 50);
// Logs “undefined roared”

Like our first example of this section, there is no => thus roar() has no access to DinoBuilder’s this context.

The setTimeout(dino.roar, 50) will output undefined roared as it has currently been implemented.

class DinoBuilder {
  dinoName = "Trex";
  roar = () => {
    console.info(`${this.dinoName} roared.`);
  };
}

const dino = new DinoBuilder();

setTimeout(dino.roar, 50);
// Logs “Trex roared”

While the syntax is a bit different from our second example, this still uses the power of arrow functions.

With the =>, the roar method reaches the DinoBuilder’s this context. So roar outputs Trex roared.

Setup 1

✏️ Create src/classes/dino.ts and update it to be:

function DinoKeeper(name) {
  this.name = name;
}

DinoKeeper.prototype.sayHi = function () {
  return this.name + ' says “hi”';
};

const employee1 = new DinoKeeper("Joe");
employee1.sayHi();
// Joe says “hi”

Verify 1

✏️ Create src/classes/dino.test.ts and update it to be:

import DinoKeeper from "./dino";
import assert from 'node:assert/strict';
import { describe, it } from "node:test";

describe("Classes: DinoKeeper", () => {
  it("basics work", () => {
    const dinoKeeper = new DinoKeeper("Joe");
    assert.equal(dinoKeeper.sayHi(), `Joe says “hi”`);
  });

  it("typing works", () => {
    const dinoKeeper = new DinoKeeper("Joe") as DinoKeeper;
    assert.equal(dinoKeeper.sayHi(), `Joe says “hi”`);
  });
});

Exercise 1

In this exercise, we will take an old-school JavaScript class and convert it to a shiny new TypeScript class.

Have issues with your local setup? You can use either StackBlitz or CodeSandbox to do this exercise in an online code editor.

Solution 1

If you’ve implemented the solution correctly, the tests will pass when you run npm run test!

Click to see the solution

✏️ Update src/classes/dino.ts to be:

class DinoKeeper {
  name: string;

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

  sayHi(): string {
    return `${this.name} says “hi”`;
  }
}
const employee1 = new DinoKeeper("Joe");
employee1.sayHi();
// Logs "Joe says “hi”"

export default DinoKeeper;

Have issues with your local setup? See the solution in StackBlitz or CodeSandbox.

Objective 2: Extend a class

Inheritance

Inheritance is a way to extend functionality of existing classes. If the derived class contains its own constructor function, it MUST call a super method with parameters matching that of its parent class.

The super is a call to the parent constructor method to ensure the properties are set for the parent. The following shows accessing the move method from the parent class and adding run and talk methods to the child class.

class Dinosaur {
  constructor(public name: string) {}

  move(distanceInFeet: number = 0): void {
    console.info(`${this.name} moved ${distanceInFeet} feet.`);
  }
}

class Velociraptor extends Dinosaur {
  constructor(
    name: string,
    public speed: number,
  ) {
    super(name);
  }
  run(): void {
    console.info(`${this.name} runs at ${this.speed}mph.`);
  }
  talk(): void {
    console.info(`${this.name} screeches.`);
  }
}

let blue = new Velociraptor("Blue", 55);
blue.move(10);
// Logs "Blue moved 10 feet."
blue.talk();
// Logs "Blue screeches."
blue.run();
// Logs "Blue runs at 55mph."

The public modifier

In TypeScript, all fields are public by default, meaning they can be accessed from outside the class.

class Dinosaur {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  public walk(distanceInFeet: number): void {
    console.info(`${this.name} walked ${distanceInFeet} feet.`);
  }
}

const myDino = new Dinosaur("Mildred");
console.info(myDino.name);
myDino.walk(7);

The highlighted property name can be accessed as the instance of Dinosaur. As explained above, by default all fields are public, so it will be accessible even without the public keyword.

However, in classes with many properties and methods or in cases where a specific coding style is required, it might be necessary to explicitly declare properties as public, just as we did with the walk method.

For the Dinosaur instance named myDino, both the name field and the walk method are accessible externally, so accessing myDino.name will return "Mildred", and calling myDino.walk(7) will output "Mildred walked 7 feet."

Setup 2

✏️ Create src/classes/specialist.ts and update it to be:

class DinoKeeper {
  name: string;

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

  sayHi(): string {
    return `${this.name} says “hi”`;
  }
}
const employee1 = new DinoKeeper("Joe");
employee1.sayHi();

class Specialist {}
export default Specialist;

Verify 2

✏️ Create src/classes/specialist.test.ts should look like:

import Specialist from "./specialist";
import assert from 'node:assert/strict';
import { describe, it } from "node:test";

function removeSpaces(str: string) {
  return str.replace(/\s+/g, " ");
}

describe("Classes: Specialist", () => {
  it("basics work", () => {
    const employee2 = new Specialist("Owen", 14);
    assert.equal(employee2.sayHi(), `Owen says “hi”`);
    assert.equal(
      removeSpaces(employee2.safetyQuote()),
      removeSpaces(`Never turn your back to the cage.
                Trust me, I have 14 years of experience`)
    );
  });
});

Exercise 2

In this exercise, we will write a new Specialist class. This new Specialist class should:

  • Inherit from DinoKeeper.
  • Accept an additional experience public field that is a number.
  • Have a safetyQuote method that returns "Never turn your back to the cage. Trust me, I have ${experience} years of experience".

For example, you should be able to use Specialist as follows:

const employee2 = new Specialist("Owen", 14);
employee2.sayHi(); // Owen says 'hi'
employee2.safetyQuote();
// Logs "Never turn your back to the cage. Trust me, I have 14 years of experience"

Have issues with your local setup? You can use either StackBlitz or CodeSandbox to do this exercise in an online code editor.

Solution 2

If you’ve implemented the solution correctly, the tests will pass when you run npm run test!

Click to see the solution

✏️ Update src/classes/specialist.ts to be:

class DinoKeeper {
  name: string;

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

  sayHi(): string {
    return `${this.name} says “hi”`;
  }
}
const employee1 = new DinoKeeper("Joe");
employee1.sayHi();

class Specialist extends DinoKeeper {
  constructor(name: string, public experience: number) {
    super(name);
  }

  safetyQuote() {
    return `Never turn your back to the cage.
    Trust me, I have ${this.experience} years of experience`;
  }
}
export default Specialist;

Have issues with your local setup? See the solution in StackBlitz or CodeSandbox.

Objective 3: Additional assignment modifiers

The private modifier

Fields marked private are unable to be accessed from outside their containing class.

class Dinosaur {
  public name: string;
  private dna: string;
  constructor(name: string, dna: string) {
    this.name = name;
    this.dna = dna;
  }
  public walk(distanceInFeet: number): void {
    console.info(`${this.name} walked ${distanceInFeet} feet.`);
  }
}

let scaryDino = new Dinosaur("Indominous", "cuttlefish");
scaryDino.dna; // Error: Property 'dna' is private and only accessible within class 'Dinosaur'.ts(2341)

The protected modifier

The protected modifier is similar to the private modifier in that it makes properties that can’t be accessed from outside of the class. The main exception is that protected properties are accessible by classes that inherit from it.

The following example shows an inherited class that can access its parent protected property teethCount:

class Dinosaur {
  public name: string;
  private dna: string;
  protected teethCount: number;
}

// EFFECT ON INSTANCES
var indominusRex = new Dinosaur();
indominusRex.name; // Okay
indominusRex.dna; // Error: Property 'dna' is private and only accessible within class 'Dinosaur'.ts(2341)
indominusRex.teethCount; // Error: Property 'teethCount' does not exist on type 'Dinosaur'.ts(2339)

// EFFECT ON CHILD CLASSES
class GeneticallyModifiedDinosaur extends Dinosaur {
  constructor() {
    super();
    this.name; // Okay
    this.dna; // Error: Property 'dna' is private and only accessible within class 'Dinosaur'.ts(2341)
    this.teethCount; // Okay
  }
}

The readonly modifier

The readonly modifier allows properties to be read, but not changed after initialization. That means that readonly fields can be accessed outside the class, but their value can’t be changed.

class Leoplurodon {
  readonly location: string;
  readonly numberOfFlippers = 4;
  readonly magic = true;
  constructor(theLocation: string) {
    this.location = theLocation;
  }

  updateLocation(location: string): void {
    this.location = location; // Error: Cannot assign to 'location' because it is a read-only property.ts(2540)
  }
}
let firstStop = new Leoplurodon("On the way to Candy Mountain");
firstStop.location = "On a bridge"; // Error: Cannot assign to 'location' because it is a read-only property.ts(2540)

Next steps

Next on the chopping block is working with Interfaces in Typescript.