class Sampler {
greeting: string;
constructor (message: string) {
this.greeting = message;
}
greet () {
return "Hello, " + this.greeting;
}
}
let sampler = new Sampler("world");
console.log(sampler.greet()); // Hello, world
Inheritance is another paradigm that is one of the cornerstones of object-oriented programming.Inheritance means that an object uses another object as its base type, thereby inheriting all the characteristics of the base object, including all the properties and functions.
Both interfaces and classes can use inheritance.
class Animal {
name: string;
constructor (theName: string) {
this.name = theName;
}
move (distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Frog extends Animal {
constructor (name: string) {
super(name);
}
move (distanceInMeters = 5) {
console.log("Jumping...");
super.move(distanceInMeters);
}
}
class Horse extends Animal {
constructor (name: string) {
super(name);
}
move (distanceInMeters = 45) {
console.log("Galloping...");
super.move(distanceInMeters);
}
}
let jack = new Frog("Froggy the Traveller");
let nick: Animal = new Horse("MrHorse");
jack.move();
nick.move(34);
Derived classes containing constructor functions must call super()
, which will execute the constructor function of the base class.
interface IBase {
id: number | undefined;
}
interface IDerivedFromBase extends IBase {
name: string | undefined;
}
class InterfaceInheritance implements IDerivedFromBase {
id: number | undefined;
name: string | undefined;
}
Although we only have the properties shown in this example, the same rules apply for functions.
class BaseClass implements IBase {
id: number | undefined;
}
class DerivedFromBaseClass extends BaseClass implements IDerivedFromBase {
name: string | undefined;
}
The second class, DerivedFromBaseClass, not only inherits from the BaseClass class (using the extends keyword) but also implements the IDerivedFromBase interface. Since BaseClass already defines the id property required in the IDerivedFromBase interface, the only other property that the DerivedFromBaseClass class must implement is the name property. Therefore, we need to include the definition of only this property in the DerivedFromBaseClass class.
TS does not support the concept of multiple inheritance. Multiple inheritance means that a single class can be derived from multiple base classes. TS supports only single inheritance, and therefore any class can have only one base class.
However, a class can implement many interfaces:
interface IFirstInterface {
id: number | undefined;
}
interface ISecondInterface {
name: string | undefined;
}
class MultipleInterfaces implements IFirstInterface, ISecondInterface {
id: number | undefined;
name: string | undefined;
}
This means that the MultipleInterfaces class must implement the id property to satisfy the IFirstInterface interface, and the name property to satisfy the IFirstInterface interface.
Access modifiers allow you to hide the state of an object from external access and control access to this state. TS has three modifiers: public
, protected
, and private
.
In TS, each class member will be public by default. But we can mark the members of the class public explicitly.
class Animal {
public name: string;
public constructor(theName: string) {
this.name = theName;
}
public move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
When a class member is marked with the private modifier, it cannot be accessed outside of that class.
class Animal {
private name: string;
constructor(theName: string) {
this.name = theName;
}
}
new Animal("Cat").name; // Error: 'name' is private;
TS is a structured type system. When we compare two different types, regardless of where and how they are described and implemented, if the types of all their members are compatible, it can be argued that the types themselves are compatible.
However, when comparing types with the private access modifier, this happens differently. Two types will be considered compatible if both members have the private modifier from the same declaration. This also applies to protected members.
class Animal {
private name: string;
constructor(theName: string) {
this.name = theName;
}
}
class Cat extends Animal {
constructor () {
super("Cat");
}
}
class Employee {
private name: string;
constructor(theName: string) {
this.name = theName;
}
}
let animal = new Animal("Goat");
let cat = new Cat();
let employee = new Employee("Bob");
animal = cat;
// animal = employee; // Error: Animal and Employee are not compatible
// Types have separate declarations of a private property 'name'.
Even though Employee has a private member named name, this is not the member we declared in Animal.
The protected modifier acts similarly to private, except that members declared by protected can be accessed in subclasses.
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;
}
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch()); // "Hello, my name is Howard and I work in Sales."
// console.log(howard.name); // error
The constructor can also have the protected modifier.This means that a class cannot be created outside of the class that contains it, but it can be inherited.
class Person {
protected name: string;
protected constructor(theName: string) {
this.name = theName;
}
}
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: The 'Person' constructor is protected
// Constructor of class 'Person' is protected and only accessible within the class declaration.
You can make properties read-only by using the readonly keyword. Readonly properties must be initialized when they are declared or in the constructor.
class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor (theName: string) {
this.name = theName;
}
}
let dad = new Octopus('Man with the 8 strong legs');
// dad.name = 'Man with the 3-piece suit'; // Cannot assign to 'name' because it is a read-only property.
By using the optional +
sign along with the type modifiers, we can create more explicit and readable type declarations. We can also use the -
(minus) sign to remove optional declarations from the ?
properties.
interface ICar {
name: string;
age: number;
}
type ReadonlyCar = {
readonly [K in keyof ICar] : ICar[K];
}
Type iterator modifiers make it easy to extend existing types and apply massive changes to all their properties.
const car: ICar = {
name: 'Mercedes',
age: 2
};
const readOnlyCar: ReadonlyCar = {
name: 'BMW',
age: 5
};
car.age = 3;
// readOnlyCar.age = 6; // Error: Cannot assign to 'age' because it is a read-only property.
We can specify that all properties are optional via ?
.
type ReadonlyCar = {
readonly [K in keyof ICar]?: ICar[K];
};
Also, we can specify that all properties are strings, or make each property as a union of their original type and string through a vertical bar |
.
type ReadonlyCar = {
readonly [K in keyof ICar]?: ICar[K] | string;
};
We can remove the flag not only with ?
sign. Starting with TS 2.8, it became possible to add a minus sign (-
) before the character that we want to remove.
type ReadonlyCar = {
readonly [K in keyof ICar]-?: ICar[K];
};
Since we have the flexibility with the -
sign to remove flags from our types, the +
sign has also been added to this feature. We can say more clearly what we are adding and what we are removing.
type ReadonlyCar = {
+readonly [K in keyof ICar] -?: ICar[K];
};
Type iterator modifiers are useful if:
The parameter properties allow you to create and initialize members in one place.
class Octopus {
readonly numberOfLegs: number = 8;
constructor (readonly name: string) {
}
}
Parameter properties are declared before a constructor parameter that has an availability modifier, readonly, or both. Using the private parameter property declares and initializes the private member; so do public, protected, and readonly.
TS supports getters and setters as a way to intercept accesses to object properties. This gives you more control over the moment you interact with the properties of objects.
class Employee {
fullName: string;
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
console.log(employee.fullName);
}
Allowing fullName to be set directly is quite convenient, but it can lead to problems if someone wants to change the name at will.
let password = 'secret password';
class Employee {
private _fullName: string;
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (password && password == 'secret password') {
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);
}
We can also create static class members, those that are visible in the class without creating an instance.
class Grid {
static origin = {x: 0, y: 0};
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;
}
constructor (public scale: number) {
}
}
let grid1 = new Grid(1.0); // 1x scale
let grid2 = new Grid(5.0); // 5x scale
console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
Abstract classes are base classes from which others inherit. Their instances cannot be created directly. Unlikean interface, an abstract class can contain the implementation details of its members. The abstract
keyword is used to define abstract classes, as well as abstract methods within such classes.
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log('roaming the earth...');
}
}
Methods within an abstract class that are marked as abstract do not contain an implementation and must beimplemented in derived classes.
abstract class Department {
constructor(public name: string) {
}
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'); // constructors in derived classes must call super()
}
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 a non-abstract subclass
department.printName(); // ok to call an abstract method
department.printMeeting(); // ok to call a concrete method
department.generateReports(); // error: method doesn't exist on declared abstract type
When you declare a class in TS, you are actually creating multiple declarations at the same time.
The first declaration is of the class instance type.
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter: Greeter;
greeter = new Greeter("world");
console.log(greeter.greet()); // Hello, world
We also create another value, which is called a constructor function. This function is called when we create instances of the class using new. To see how this looks in practice, let’s look at the JavaScript code generated by the compiler from the example above:
let Greeter = (function() {
function Greeter(message) {
this.greeting = message;
}
Greeter.prototype.greet = function() {
return "Hello, " + this.greeting;
};
return Greeter;
})();
let greeter;
greeter = new Greeter("world");
console.log(greeter.greet()); // Hello, world
Another way to think about each class is: there is an instance part and a static part.
class Greeter {
static standardGreeting = "Hello, there";
greeting: string;
greet() {
if (this.greeting) {
return "Hello, " + this.greeting;
}
else {
return Greeter.standardGreeting;
}
}
}
let greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet()); // Hello, there
let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";
let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet()); // Hey there!
As we discussed in the previous section, a class declaration creates two things: a type that describes instances of the class, and a constructor function. Since classes create types, we can use them in the same way as interfaces.
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};