TypeScript classes, interfaces, inheritance

Posted by kostik on Sun, 19 May 2019 17:24:11 +0200

TS introduces the concept of Class as a template for objects. Classes can be defined by the class keyword.
Basically, TS's class can be regarded as a grammatical sugar, and most of its functions can be achieved by ES5. The new class writing just makes the object prototype writing more clear and more like the grammar of object-oriented programming.

class

Define a class

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

Use this class

let p=new Point(1,2);

The following points should be noted:
(1) Strict mode is adopted by default within classes and modules, and no use strict is required to specify the mode of operation.

(2) The constructor method is the default method of the class, which is called automatically when the object instance is generated by the new command. A class must have constructor methods, and if there is no explicit definition, an empty constructor method will be added by default, which is consistent with Java classes.

(3) class must be invoked using the new command, otherwise an error will be reported.

(4) There is no variable escalation in a class. Classes can only be used if they are declared first.

If this is included in the method of the class, it defaults to point to the instance of the class. But if we extract the method separately, this value may point to the current running environment. To prevent this from happening, we can use the arrow function (the value of this of the arrow function points to the initialized function).

public, private, protected and readonly

public, private, protected, and readonly are members (attributes) modifiers of classes
public
In TS, members default to public. Attributes modified by public are freely accessible both inside and outside the class.

class Animal {
    public name: string;
    public constructor(theName: string) { this.name = theName; }
    }
 new Animal("Cat").name;//Cat

private
When a member is marked private, it cannot declare external access to its classes.

class Animal {
     private name: string;
     constructor(theName: string) { this.name = theName; }
    }
 new Animal("Cat").name;//Error!: Property 'name' is private and only accessible within class 'Animal'.

TS uses a structured type system. When we compare two different types, we don't care where they come from. If all members'types are compatible, we think their types are compatible.

The comparison here is not what we call the comparison of=== or=====, but the comparison of the expected value (structure).

class Animal1 {
     name: string;
    constructor(theName: string) { this.name = theName; }
}
class Animal2 {
    name: string;
   constructor(theName: string) { this.name = theName; }
}
//There is no mistake in this way of writing.
let a:Animal1=new Animal2("cat");

However, the types of members modified by private or protected are different. If one type contains a private (or protected) member, the two types are compatible, otherwise they are incompatible, when the other type also has such a private (or protected) member, and they all come from the same declaration.

class Animal1 {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

class Animal2  extends Animal1{
    constructor(theName: string) {super(name); }
}

class Animal3 {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

let animal1: Animal1 = new Animal2("cat");//That's all right. The private ly modified member variable name of Animal1 and Animal2 comes from Animal1 (both from the same declaration).

let animal3: Animal1 = new Animal3("cat");//ERROR:Type 'Animal3' is not assignable to type 'Animal3'.

protected
Protected modifiers behave very similar to private modifiers, but one difference is that protected members are still accessible in derived classes.
Derived classes cannot be accessed with private ly modified parent class members.

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

class Employee extends Person {
    constructor(name: string) { super(name)}
    public sayName() {
        return this.name;//ERROR!: Property 'name' is private and only accessible within class 'Person'.
    }
}

let xiaoming = new Employee("xiaoming");
console.log(xiaoming.sayName());

protected modified parent class members are still accessible in derived classes

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

class Employee extends Person {
    constructor(name: string) { super(name)}
    public sayName() {
        return this.name;
    }
}
//Can still be used in derived classes
let xiaoming = new Employee("xiaoming");
console.log(xiaoming.sayName());

readonly modifier
Unlike public, private and protected keywords, readonly keywords modify not members'access rights, but members' reassignment rights.
Use the readonly keyword to set the property to read-only. Read-only properties must be initialized at declaration time or in constructors.

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.

abstract class

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

abstract class Animal {
    abstract makeSound(): void;// Must be implemented in derived classes
    move(): void {
        console.log('roaming the earch...');
    }
}

Be careful:
Abstract methods in abstract classes do not contain concrete implementations and must be implemented in derived classes.
(2) An abstract method must contain Abstract keywords and can contain access modifiers.

Interface

In traditional object-oriented concepts, one class can extend another class or implement one or more interfaces. An interface can implement one or more interfaces but cannot extend another class or interface. The wiki encyclopedia defines the interface in OOP as:

In object-oriented languages, the term "interface" is often used to define an abstract type of behavior that does not contain data and logical code but uses function signatures.

However, for TS, the more important meaning of interfaces is to type check the structure of values.
Interfaces can be divided into three categories according to their attributes. One is the optional attributes, the other is the optional attributes, and the other is the read-only attributes.

Required attribute

The required attributes are the attributes that a function must have.

interface PersonVaule{
    name:string;
    age:number;
}
function Person(person:PersonVaule){
    this.name=person.name;
    this.age=person.age;
}
//Create examples
var xiaoming=new Person({name:"xiaoming",age:18})

Type checkers do not check the order of attributes, but they must be selected.

var xiaoming2=new Person({age:18,name:"xiaoming"})//No problem

var xiaoming3=new Person({name:"xiaoming"})//Deletion of prompt attributes:Property 'age' is missing in type '{ name: string; }'.

optional attribute

Attributes in interfaces are not all required. Some exist only under certain conditions or do not exist at all. Optional attributes are often used in the application of the "option bags" pattern, that is, only part of the parameter objects passed in by functions are assigned values.

Interfaces with optional attributes are similar to ordinary interface definitions, except that a? Symbol is added after the name definition of optional attributes.

interface AnimalVaule{
    name?:string;
    eat:string;
    lifestyle?:string;
}
function Animal(animal:AnimalVaule){
    this.name=animal.name;
    this.eat=animal.eat;
    this.lifestyle=animal.lifestyle;
}
let cat=new Animal({eat:"Carnivores",lifestyle:"Conceal oneself by day and March by night"});

Optional attributes have two advantages:

1. Possible attributes can be predefined
2. You can catch errors when referring to non-existent attributes.

The following example shows an error prompt:

let dog=new Animal({eat:"Adaptive carnivores",lifestle:"Nocturnal night"})//'lifestle' does not exist in type 'AnimalVaule'.

Read-only attribute

Some object attributes can only modify their values when the object has just been created. You can specify read-only properties with readonly before the property name:

interface Point {
    readonly x: number;
    readonly y: number;
}

You can construct a Point by assigning an object literal. After assignment, x and y can no longer be changed.

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

readonly and const
readonly and const declared variables or attributes are not allowed to be modified twice. The difference between these two attributes is whether they are used as variables or attributes:
The use of const as a variable,
readonly is used as an attribute.

Interfaces can describe not only the attributes of objects, but also function types, indexable types and class types.

Function type

In order to use interfaces to represent function types, we need to define a call signature for interfaces. It's like a function definition with only parameter lists and return value types. Each parameter in the parameter list needs a name and type.

interface SearchFunc {
    (source: string, subString: string): boolean;
}

let mySearch:SearchFunc=function(src,sub){
    let result = src.search(sub);
    return result > -1;
}

Be careful:
Function parameters are checked one by one, requiring that the parameter types at the location of the corresponding interface are compatible without name consistency.

Indexable type

Similar to using interfaces to describe function types, we can also describe those types that can be "indexed", such as a[10] or ageMap["daniel"]. The indexable type has an index signature that describes the type of object index and the corresponding index return value type.

Index signatures come in two forms: strings and numbers.

Digital index signature:

interface NN {[index: number]: number;}
let nn: NN = [1, 2];

interface NS {[index: number]: string;}
let ns: NS = ["1", "2"];

In the example above, we define the NN interface and the NS interface, which have index signatures. This index signature represents the return value of the number type or string when the number is used to index the NN or NS interface.

String index signature:
String index signatures describe dictionary patterns well, and they also ensure that all attributes match their return value types.

interface SS {[index:string]:string}
let ss: SS = {"A":"a", "B":"b"};

interface SN {[index: string]: number;}
let sn: SN = {"A":1, "B":2};

You can set the index signature to read-only, which prevents assigning values to the index:

interface ReadonlyStringArray {
    readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!

The return value of an index can be more than one, but it must be of the same type.

interface NN {
    [index: number]: number;
    length:number;
    name: string       // Error, `name'type does not match the type of index type return value
}
let nn: NN = [1, 2];

Note: If there are multiple return values, the return value of the digital index must be a subtype of the return value type of the string index.
For the above explanation, the original words of TS are as follows:

This is because when indexed with number, JavaScript converts it to string and then to index objects. That is to say, using 100 (a number) to index is equivalent to using "100" (a string) to index, so they need to be consistent.

Although the literal explanation is not clear, we can understand its meaning through examples.

class Animal {
    name: string;
}
class Dog extends Animal {
    breed: string;
}

//ERROR!: Numeric index type 'Animal' is not assignable to string index type 'Dog'.
interface NotOkay {
    [x: number]: Animal;
    [x: string]: Dog;
}

For the example above, the return value of the number index is the parent Animal, while the return value of the string index is the child Dog. So TS reported an error.

If the return value of the modified number index is the subclass Dog and the return value of the string index is the parent Animal, then there is no problem.

class Animal {
    name: string;
}
class Dog extends Animal {
    breed: string;
}

interface Okay {
    [x: number]: Dog;
    [x: string]: Animal;
}

Class type

Like the basic function of interfaces in C# or Java, TS can use it to explicitly force a class to conform to a contract.

interface ClockInterface {
    currentTime: Date;
}

class Clock implements ClockInterface {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

Note: The interface describes the public part of the class, not the public and private parts. It doesn't help you check whether a class has some private members.

inherit

TS allows us to create subclasses (to implement inheritance) through the extends keyword.
In the following example, the Dog class inherits from the Animal class, in which we can access the properties and methods of the parent Animal class.

class Animal {
    name: string;
    constructor(theName: string) { this.name = theName; }
}
class Dog extends Animal {
    breed: string;
}

new Dog("mydog").name;//mydog

Note: Derived classes containing constructors must call super(), which executes the construction method of the base class.

Reference material: TS library on github

Topics: Attribute Java Programming Javascript