Classes
# Classes
Traditional JavaScript programs use functions and prototype-based inheritance to build reusable components, but this feels awkward for programmers more comfortable with an object-oriented approach where classes are the foundation and objects are built from classes. Starting with ECMAScript 2015 (ES6), JavaScript programmers can build their applications using class-based object-oriented programming. With TypeScript, we allow developers to use these features now, and the compiled JavaScript runs on all major browsers and platforms without waiting for the next JavaScript version.
# Basic Example
Let's look at an example using classes:
class Greeter {
greeting: string
constructor(message: string) {
this.greeting = message
}
greet() {
return 'Hello, ' + this.greeting
}
}
let greeter = new Greeter('world')
2
3
4
5
6
7
8
9
10
11
If you've used C# or Java, this syntax will look very familiar. We declare a Greeter class. This class has 3 members: a property called greeting, a constructor, and a greet method.
You'll notice that we use this when referring to any class member. This indicates we're accessing a class member.
On the last line, we construct an instance of the Greeter class using new. This calls the previously defined constructor, creates a new Greeter object, and runs the constructor to initialize it.
# Inheritance
In TypeScript, we can use common object-oriented patterns. One of the most fundamental patterns in class-based programming is being able to extend existing classes using inheritance.
Let's look at an example:
class Animal {
move(distance: number = 0) {
console.log(`Animal moved ${distance}m.`)
}
}
class Dog extends Animal {
bark() {
console.log('Woof! Woof!')
}
}
const dog = new Dog()
dog.bark()
dog.move(10)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
This example shows the most basic inheritance: classes inherit properties and methods from base classes. Here, Dog is a derived class that derives from the Animal base class using the extends keyword. Derived classes are often called subclasses, and base classes are often called superclasses.
Because Dog inherits the functionality of Animal, we can create an instance of Dog that can both bark() and move().
Let's look at a more complex example.
class Animal {
name: string
constructor(name: string) {
this.name = name
}
move(distance: number = 0) {
console.log(`${this.name} moved ${distance}m.`)
}
}
class Snake extends Animal {
constructor(name: string) {
super(name)
}
move(distance: number = 5) {
console.log('Slithering...')
super.move(distance)
}
}
class Horse extends Animal {
constructor(name: string) {
super(name)
}
move(distance: number = 45) {
console.log('Galloping...')
super.move(distance)
}
}
let sam = new Snake('Sammy')
let tom: Animal = new Horse('Tommy')
sam.move()
tom.move(34)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
This example demonstrates some features not mentioned above. This time, we use the extends keyword to create two subclasses of Animal: Horse and Snake.
The difference from the previous example is that derived classes contain a constructor, which must call super(), which executes the base class constructor. Moreover, before accessing any property on this in the constructor, we must call super(). This is an important rule enforced by TypeScript.
This example also shows how to override methods from the parent class in subclasses. Both Snake and Horse create a move method that overrides the move inherited from Animal, giving move different functionality depending on the class. Note that even though tom is declared as Animal, since its value is Horse, calling tom.move(34) will call the overridden method in Horse.
Slithering...
Sammy moved 5m.
Galloping...
Tommy moved 34m.
2
3
4
# Public, Private, and Protected Modifiers
# Default to public
In the above examples, we can freely access the members defined in the program. If you're familiar with classes in other languages, you'll notice we didn't use public to qualify anything; for example, C# requires that you explicitly use public to make members visible. In TypeScript, members default to public.
You can also explicitly mark a member as public. We can rewrite the Animal class above as follows:
class Animal {
public name: string
public constructor(name: string) {
this.name = name
}
public move(distance: number) {
console.log(`${this.name} moved ${distance}m.`)
}
}
2
3
4
5
6
7
8
9
# Understanding private
When a member is marked private, it cannot be accessed from outside its declaring class. For example:
class Animal {
private name: string
constructor(name: string) {
this.name = name
}
}
new Animal('Cat').name // Error: 'name' is private.
2
3
4
5
6
7
8
TypeScript uses a structural type system. When we compare two different types, regardless of where they came from, if the types of all members are compatible, then we consider the types themselves to be compatible.
However, when comparing types with private or protected members, things are different. If one type has a private member, then the other type must also have a private member originating from the same declaration for us to consider them compatible. The same rule applies to protected members.
Let's look at an example to better illustrate this:
class Animal {
private name: string
constructor(name: string) {
this.name = name
}
}
class Rhino extends Animal {
constructor() {
super('Rhino')
}
}
class Employee {
private name: string
constructor(name: string) {
this.name = name
}
}
let animal = new Animal('Goat')
let rhino = new Rhino()
let employee = new Employee('Bob')
animal = rhino
animal = employee // Error: Animal and Employee are not compatible.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
In this example, we have Animal and Rhino, where Rhino is a subclass of Animal. We also have an Employee class that looks identical to Animal in terms of shape. We create instances of these classes and try to assign them to each other to see what happens. Because Animal and Rhino share the private member definition private name: string from Animal, they are compatible. However, Employee is not. When assigning Employee to Animal, we get an error saying their types are incompatible. Even though Employee also has a private member name, it's clearly not the one declared in Animal.
# Understanding protected
The protected modifier acts similarly to private, but with one difference: protected members are still accessible in derived classes. For example:
class Person {
protected name: string
constructor(name: string) {
this.name = name
}
}
class Employee extends Person {
private department: string
constructor(name: string, department: string) {
super(name)
this.department = department
}
getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`
}
}
let howard = new Employee('Howard', 'Sales')
console.log(howard.getElevatorPitch())
console.log(howard.name) // error
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Note that we can't use name outside of Person, but we can still access it from within an instance method of Employee because Employee derives from Person.
A constructor can also be marked protected. This means the class cannot be instantiated outside of its containing class, but can be extended. For example:
class Person {
protected name: string
protected constructor(name: string) {
this.name = name
}
}
// Employee can extend Person
class Employee extends Person {
private department: string
constructor(name: string, department: string) {
super(name)
this.department = department
}
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`
}
}
let howard = new Employee('Howard', 'Sales')
let john = new Person('John') // Error: 'Person' constructor is protected.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# readonly Modifier
You can make properties read-only by using the readonly keyword. Read-only properties must be initialized at their declaration or in the constructor.
class Person {
readonly name: string
constructor(name: string) {
this.name = name
}
}
let john = new Person('John')
john.name = 'peter'
2
3
4
5
6
7
8
9
# Parameter Properties
In the example above, we had to declare a read-only member name in the Person class and a constructor parameter name, then immediately assign name to this.name -- a common scenario. Parameter properties let us conveniently create and initialize a member in one place. Below is a modified version of the Person class using parameter properties:
class Person {
constructor(readonly name: string) {
}
}
2
3
4
Notice how we dropped the name parameter and instead used readonly name: string in the constructor to create and initialize the name member. We merged the declaration and assignment into one place.
Parameter properties are declared by adding an accessibility modifier before a constructor parameter. Using private for a parameter property declares and initializes a private member; the same goes for public and protected.
# Accessors
TypeScript supports getters/setters to intercept access to object members. This gives you a way to have fine-grained control over how a member is accessed on each object.
Let's see how to convert a simple class to use get and set. First, let's start without accessors.
class Employee {
fullName: string
}
let employee = new Employee()
employee.fullName = 'Bob Smith'
if (employee.fullName) {
console.log(employee.fullName)
}
2
3
4
5
6
7
8
9
We can set fullName because it's public. Sometimes we want to trigger extra logic when modifying it -- that's where accessors come in handy.
In this version, we first check that the user password is correct before allowing them to modify employee information. We replace direct access to fullName with a set accessor that checks the password. We also add a get accessor so the example above still works.
let passcode = 'secret passcode'
class Employee {
private _fullName: string
get fullName(): string {
return this._fullName
}
set fullName(newName: string) {
if (passcode && passcode == 'secret passcode') {
this._fullName = newName
}
else {
console.log('Error: Unauthorized update of employee!')
}
}
}
let employee = new Employee()
employee.fullName = 'Bob Smith'
if (employee.fullName) {
console.log(employee.fullName)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
We can change the password to verify that the accessor is working. When the password is incorrect, we're notified that we don't have permission to modify the employee.
A couple of things to note about accessors:
First, accessors require you to set the compiler to output ECMAScript 5 or higher. Downleveling to ECMAScript 3 is not supported. Second, accessors with only a get and no set are automatically inferred as readonly. This is helpful when generating .d.ts files, because users of this property will see that it can't be changed.
# Static Properties
So far, we've only discussed instance members of the class -- those that are initialized only when the class is instantiated. We can also create static members of a class, which exist on the class itself rather than on instances. In this example, we use static on origin because it's a property used by all grids. Each instance accesses this property by prepending the class name. Just as we use this.xxx for instance properties, here we use Grid.xxx for static properties.
class Grid {
static origin = {x: 0, y: 0}
scale: number
constructor (scale: number) {
this.scale = scale
}
calculateDistanceFromOrigin(point: {x: number; y: number}) {
let xDist = point.x - Grid.origin.x
let yDist = point.y - Grid.origin.y
return Math.sqrt(xDist * xDist + yDist * yDist) * this.scale
}
}
let grid1 = new Grid(1.0) // 1x scale
let grid2 = new Grid(5.0) // 5x scale
console.log(grid1.calculateDistanceFromOrigin({x: 3, y: 4}))
console.log(grid2.calculateDistanceFromOrigin({x: 3, y: 4}))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Abstract Classes
Abstract classes are base classes from which other classes may be derived. They are generally not directly instantiated. Unlike interfaces, abstract classes may contain implementation details for their members. The abstract keyword is used to define abstract classes and abstract methods within them.
abstract class Animal {
abstract makeSound(): void
move(): void {
console.log('roaming the earth...')
}
}
2
3
4
5
6
Abstract methods within an abstract class do not contain an implementation and must be implemented in derived classes. Abstract method syntax is similar to interface method syntax. Both define method signatures without method bodies. However, abstract methods must include the abstract keyword and may contain access modifiers.
abstract class Department {
name: string
constructor(name: string) {
this.name = name
}
printName(): void {
console.log('Department name: ' + this.name)
}
abstract printMeeting(): void // must be implemented in derived classes
}
class AccountingDepartment extends Department {
constructor() {
super('Accounting and Auditing') // must call super() in derived class constructors
}
printMeeting(): void {
console.log('The Accounting Department meets each Monday at 10am.')
}
generateReports(): void {
console.log('Generating accounting reports...')
}
}
let department: Department // ok to create a reference to an abstract type
department = new Department() // error: cannot create an instance of an abstract class
department = new AccountingDepartment() // ok to create and assign an abstract subclass
department.printName()
department.printMeeting()
department.generateReports() // error: method doesn't exist on the declared abstract type
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Advanced Techniques
# Constructors
When you declare a class in TypeScript, you are actually declaring multiple things at once. The first is the type of the class instance.
class Greeter {
static standardGreeting = 'Hello, there'
greeting: string
constructor(message: string) {
this.greeting = message
}
greet() {
return 'Hello, ' + this.greeting
}
}
let greeter: Greeter
greeter = new Greeter('world')
console.log(greeter.greet())
2
3
4
5
6
7
8
9
10
11
12
13
14
Here, when we write let greeter: Greeter, we're saying that the type of Greeter class instances is Greeter. This is familiar to programmers from other object-oriented languages.
We also create what we call a constructor function. This is the function called when we new up instances of the class. Let's see what the above code looks like when compiled to JavaScript:
var Greeter = /** @class */ (function () {
function Greeter(message) {
this.greeting = message;
}
Greeter.prototype.greet = function () {
return 'Hello, ' + this.greeting;
};
Greeter.standardGreeting = 'Hello, there';
return Greeter;
}());
var greeter;
greeter = new Greeter('world');
console.log(greeter.greet());
2
3
4
5
6
7
8
9
10
11
12
13
In the above code, var Greeter is assigned the constructor function. When we call new and execute this function, we get an instance of the class. This constructor function also contains all the static properties of the class. In other words, we can think of a class as having an instance side and a static side.
Let's modify the example slightly to see the differences:
class Greeter {
static standardGreeting = 'Hello, there'
greeting: string
constructor(message?: string) {
this.greeting = message
}
greet() {
if (this.greeting) {
return 'Hello, ' + this.greeting
} else {
return Greeter.standardGreeting
}
}
}
let greeter: Greeter
greeter = new Greeter()
console.log(greeter.greet())
let greeterMaker: typeof Greeter = Greeter
greeterMaker.standardGreeting = 'Hey there'
let greeter2: Greeter = new greeterMaker()
console.log(greeter2.greet())
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
In this example, greeter1 works just as we've seen before. We instantiate the Greeter class and use this object -- same as before.
Next, we use the class directly. We create a variable called greeterMaker. This variable holds the class itself, or put another way, holds the constructor function. Then we use typeof Greeter, which means "give me the type of Greeter itself" rather than the instance type. Or more precisely, "tell me the type of the Greeter identifier" -- which is the constructor function's type. This type contains all the static members and the constructor. After that, just like before, we use new on greeterMaker to create a new Greeter instance.
# Using a Class as an Interface
As mentioned in the previous section, a class declaration creates two things: the instance type of the class and a constructor function. Because classes create types, you can use them wherever interfaces are accepted.
class Point {
x: number
y: number
}
interface Point3d extends Point {
z: number
}
let point3d: Point3d = {x: 1, y: 2, z: 3}
2
3
4
5
6
7
8
9
10