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
, andreadonly
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.