“This is the fifth day of my participation in the Gwen Challenge in November. Check out the details: The Last Gwen Challenge in 2021.”

introduce

Traditional JavaScript programs use functions and prototype-based inheritance to create reusable components, but it can be tricky for programmers familiar with the object-oriented approach because they use class-based inheritance and objects are built from classes. Starting with ECMAScript 2015, also known as ECMAScript 6, JavaScript programmers will be able to use a class-based object-oriented approach. With TypeScript, we allow developers to use these features now and compile JavaScript to run on all major browsers and platforms, without waiting for the next JavaScript version

class

Here is an example of using a class:

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting; }}let greeter = new Greeter("world");
Copy the code

If you’ve ever used C# or Java, you’ll be familiar with this syntax. We declare a Greeter class. This class has three members: a property called greeting, a constructor, and a greet method

You’ll notice that we use this when referring to any of the class members. It means that we are accessing a member of the class

In the last line, we construct an instance of the Greeter class using new. It calls the previously defined constructor, creates a new object of Greeter type, and initializes it by executing the constructor

inheritance

In TypeScript, we can use common object-oriented patterns. One of the most basic patterns in class-based programming is to allow the use of inheritance to extend existing classes

class Animal {
    move(distanceInMeters: number = 0) {
        console.log(`Animal moved ${distanceInMeters}m.`); }}class Dog extends Animal {
    bark() {
        console.log('Woof! Woof! '); }}const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();
Copy the code

This example demonstrates basic inheritance: a class inherits properties and methods from a base class. Here, Dog is a derived class from the Animal base class through the extends keyword. A derived class is usually called a subclass, and a base class is usually called a superclass

Since Dog inherits Animal functionality, we can create an instance of Dog that can bark() and move().

Now let’s look at a more complicated example

class Animal {
    name: string;
    constructor(theName: string) { this.name = theName; }
    move(distanceInMeters: number = 0) {
        console.log(`The ${this.name} moved ${distanceInMeters}m.`); }}class Snake extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 5) {
        console.log("Slithering...");
        super.move(distanceInMeters); }}class Horse extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 45) {
        console.log("Galloping...");
        super.move(distanceInMeters); }}let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");

sam.move();
tom.move(34);
Copy the code

This example demonstrates some features not mentioned above. This time we use the extends keyword to create two subclasses of Animal: Horse and Snake

Unlike the previous example, the derived class contains a constructor that must call super(), which executes the constructor of the base class. Also, we must call super() before accessing the this property in the constructor. This is an important rule that TypeScript enforces

This example demonstrates how a subclass can override a parent class’s methods. The Snake class and Horse class both create move methods that override the move method inherited from Animal, so that the move method has different functions depending on the class. Notice that even though Tom is declared as Animal, since its value is Horse, tom.move(34) calls the method overridden in Horse:

// Slithering...
// Sammy the Python moved 5m.
// Galloping...
// Tommy the Palomino moved 34m.
Copy the code

Public, private, and protected modifiers

The default ispublic

In the example above, we can freely access the members defined in the program. If you’re familiar with classes in other languages, you’ll notice that we didn’t use the public modifier in the previous code; For example, C# requires that members be visible explicitly using public. In TypeScript, members default to public

You can also explicitly mark a member as public. We can rewrite the Animal class as follows:

class Animal {
    public name: string;
    public constructor(theName: string) { this.name = theName; }
    public move(distanceInMeters: number) {
        console.log(`The ${this.name} moved ${distanceInMeters}m.`); }}Copy the code

understandprivate

When a member is marked private, it cannot be accessed outside the class in which it is declared. Such as:

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }}new Animal("Cat").name; // Error: 'name' is private.
Copy the code

TypeScript uses the structural type system. When we compare two different types, we don’t care where they came from, if all members’ types are compatible, we assume they are compatible

However, when we compare types with private or protected members, the situation is different. If one of the types contains a private member, the two types are considered compatible only if there is a private member in the other type and they are both declared from the same place. Use this rule for protected members as well

Here’s an example to better illustrate the point:

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }}class Rhino extends Animal {
    constructor() { super("Rhino"); }}class Employee {
    private name: string;
    constructor(theName: string) { this.name = theName; }}let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");

animal = rhino;
animal = employee; // error: Animal is incompatible with Employee.
Copy the code

This example has two classes Animal and Rhino, which is a subclass of Animal. There is also an Employee class that looks the same type as Animal. Let’s create several instances of these classes and assign values to each other to see what happens. Animal and Rhino are compatible because they share a private member definition from Animal: String. Employee, however, is not. When assigning Employee to Animal, we get an error stating that their types are incompatible. Although Employee also has a private member name, it is clearly not the one defined in Animal

understandprotected

Protected modifiers behave much like private modifiers, with one difference: protected members are still accessible in derived classes. Such as:

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 The ${this.name} and I work in The ${this.department}. `; }}let howard = new Employee("Howard"."Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); / / error
Copy the code

Note that we can’t use name outside of the Person class, but we can still access it through an instance method of the Employee class because Employee is derived from Person

Constructors can also be marked protected. This means that the class cannot be instantiated outside the containing class, but can be inherited. Such as:

class Person {
    protected name: string;
    protected constructor(theName: string) { this.name = theName; }}// Employee can inherit 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 The ${this.name} and I work in The ${this.department}. `; }}let howard = new Employee("Howard"."Sales");
let john = new Person("John"); // Error: the constructor for 'Person' is protected.
Copy the code

Readonly modifier

You can use the readonly keyword to make the property read-only. Read-only attributes must be initialized at declaration time or in a 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"; / / error! Name is read-only.
Copy the code

Parameter properties

In the example above, we must define a read-only member name and a constructor with theName in the Octopus class, and immediately assign the value of theName to name, as is often the case. Parameter properties make it easy to define and initialize a member in one place. The following example is a modified version of the previous Octopus class that uses parameter properties:

class Octopus {
    readonly numberOfLegs: number = 8;
    constructor(readonly name: string){}}Copy the code

Notice how we discarded theName and used only the readonly name: string argument in the constructor to create and initialize theName member. We combine declaration and assignment in one place

Parameter properties are declared by prefixing constructor arguments with an access qualifier. Using private to qualify a parameter property declares and initializes a private member; The same is true for public and protected

accessor

TypeScript supports intercepting access to object members via getters/setters. It helps you effectively control access to object members

Here’s how to rewrite a simple class to use get and set. First, let’s start with an example that doesn’t use accessors

class Employee {
    fullName: string;
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    console.log(employee.fullName);
}
Copy the code

We can set fullnames arbitrarily, which is very convenient, but it can also cause trouble

In this version, we check that the user’s password is correct before allowing him or her to change employee information. We changed direct access to fullName to a set method that checks the password. We also added a get method to make the above example still work

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) {
    alert(employee.fullName);
}
Copy the code

We can change the password to verify that the accessor is working. When the password is incorrect, it will prompt us that we have no permission to change the employee

There are a few things to note about accessors:

  1. Accessors require that you set the compiler to output ECMAScript 5 or higher
  2. Downgrading to ECMAScript 3 is not supported
  3. Only withgetWithout asetThe accessor is automatically inferred toreadonly
  4. Generation from code.d.tsThis is helpful because users using this property will see that it is not allowed to change its value

Static attributes

So far, we’ve only talked about instance members of a class, attributes that are initialized only when the class is instantiated. We can also create static members of a class whose properties exist on the class itself rather than on an instance of the class. In this example, we define Origin as static because it is the property used by all grids. Every instance that wants to access this property must prefix origin with the class name. Just as we use the this. prefix on instance properties to access properties, here we use Grid. To access static properties

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}));
Copy the code

An abstract class

Abstract classes are used as base classes for other derived classes. They are generally not instantiated directly. Unlike interfaces, abstract classes can contain implementation details of members. The abstract keyword is used to define abstract classes and abstract methods within abstract classes

abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log('roaming the earch... '); }}Copy the code

Abstract methods in abstract classes contain no concrete implementation and must be implemented in derived classes. The syntax of abstract methods is similar to that of interface methods. Both define the method signature but do not contain the method body. However, abstract methods must contain the abstract keyword and can contain access modifiers

abstract class Department {

    constructor(public name: string) {
    }

    printName(): void {
        console.log('Department name: ' + this.name);
    }

    abstract printMeeting(): void; // Must be implemented in a derived class
}

class AccountingDepartment extends Department {

    constructor() {
        super('Accounting and Auditing'); // Super () must be called in the constructor of a derived class
    }

    printMeeting(): void {
        console.log('The Accounting Department meets each Monday at 10am.');
    }

    generateReports(): void {
        console.log('Generating accounting reports... '); }}let department: Department; // Allows you to create a reference to an abstract type
department = new Department(); // Error: cannot create an instance of an abstract class
department = new AccountingDepartment(); // Allows instantiation and assignment of an abstract subclass
department.printName();
department.printMeeting();
department.generateReports(); // Error: method does not exist in declared abstract class
Copy the code

Advanced techniques

The constructor

When you declare a class in TypeScript, you’re declaring many things at once. The first is the type of the instance of the class

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());
Copy the code

Here, we write let greeter: greeter, meaning that instances of a class greeter are of type greeter. This is an old habit for programmers who have worked with other object-oriented languages

We also create a value called a constructor. This function is called when we create an instance of the class using new. What does the code above look like when compiled into JavaScript

let Greeter = (function () {
    function Greeter(message) {
        this.greeting = message;
    }
    Greeter.prototype.greet = function () {
        return "Hello, " + this.greeting;
    };
    returnGreeter; }) ();let greeter;
greeter = new Greeter("world");
console.log(greeter.greet());
Copy the code

In the above code, let Greeter will be assigned to the constructor. When we call new and execute this function, we get an instance of the class. This constructor also contains all static attributes of the class. To put it another way, we can think of a class as having an instance part and a static part

Let’s rewrite the example a bit to see the difference:

class Greeter {
    static standardGreeting = "Hello, there";
    greeting: string;
    greet() {
        if (this.greeting) {
            return "Hello, " + this.greeting;
        }
        else {
            returnGreeter.standardGreeting; }}}let greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet());

let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";

let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet());
Copy the code

In this case, Greeter1 is the same as we saw before. We instantiate the Greeter class and use this object. Same thing we saw before

After that, we use classes directly. We created a variable called greeterMaker. This variable holds the class or holds the class constructor. We then use typeof Greeter, which means take the typeof the class, not the typeof the instance. Or, more accurately, “Tell me the type of the Greeter identifier,” which is the type of the constructor. This type contains all the static members and constructors of the class. Then, as before, we use new on greeterMaker to create an instance of Greeter

Use a class as an interface

Because classes create types, you can use classes where interfaces are allowed

class Point {
    x: number;
    y: number;
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1.y: 2.z: 3};
Copy the code