typescript learning notes - Interface

Keywords: Javascript Attribute TypeScript

####You need to know
One of the core principles of TypeScript is to type check the structure of a value. The purpose of an interface is to name types and define contracts or constraints for code or third-party code.
The interface between
When to use an interface, let's take a look at the following example.

function printLabel(labelledObj: { label: string }) {
   console.log(labelledObj.label);
}

The printLabel function has a parameter, and the parameter object needs to have an attribute of type string named label. The object parameters we pass in will actually contain many properties, but the compiler will only check if those necessary properties exist and if their types match. But sometimes ts is not so loose, there will be problems.
If this constraint uses an interface, it is as follows:

interface LabelledValue {
    label: string
}

function printLabel(labelledObj: LabelledValue) {
    console.log(labelledObj.label);
}
let myObj = { size: 10, label: 'Size 10 Object' };
printLabel(myObj);

This interface describes the conditions to be met by the passed in parameter object, as long as the parameter object contains a property named label and of type string.
Different from other languages, it can only be said that the object type format is constrained by the interface, not that the object implements the excuse. As mentioned above, as long as the conditions are met, the order is not within the scope of inspection.

optional attribute

The properties in the interface are not all required. Some exist only under certain conditions, or do not exist at all.
In this case, you can use optional attributes. The interface with optional attributes is similar to the ordinary interface definition, except that a? Symbol is added after the optional attribute name definition.

interface Square {
    color: string
    area: number
}

interface SquareConfig {
    color?: string
    width?: number
}

function createSquare(config: SquareConfig): Square {
    let newSquare = { color: 'white', area: 100 }
    if (config.color) {
        newSquare.color = config.color
    }
    if (config.width) {
        newSquare.area = config.width * config.width
    }
    return newSquare;
}
let mySquare = createSquare({ color: 'black' })

When the SquareConfig interface describes the parameter constraints of the createSquare function, it allows parameter objects to have color and width attributes, but it is not necessary.

Read-only attribute

Some object properties can only modify the value of an object when it was just created.
Interface read only property:

interface Point {
    readonly x: number
    readonly y: number
}
// x,y can't change
let p1: Point = { x: 10, y: 20 }
// p1.x = 5 // error

Generic read only array:

let a: number[] = [1, 2, 3, 4]
let ro: ReadonlyArray<number> = a
// ro cannot be modified, and
// A = Ro / / error a generic read-only array cannot be assigned to a normal array
// Use type assertion if assignment is required
a = ro as number[]

In addition, const can declare read-only variables. That is to say, readonly is used if read-only attribute is involved, and const is used if variable is involved.

Additional attribute checks

Look at the example directly:

interface SquareConfig {
    color?: string;
    width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
    // ...
    return
}
let mySquare = createSquare({ colooour: "red", width: 100 }); 

As shown in the above example, the call to createSquare will report an error. You might think that the parameter object contains the wdith attribute, the type is correct, there is no color attribute, and the colooour attribute can be treated as an extra meaningless attribute.
Nevertheless, TS will still think that there is BUG. Object literals are treated specially and are "extra property checked" when they are assigned to variables or passed as parameters. If an object literal has any attributes that are not included in the target type, you will get an error.
This sentence should be well understood. In the above example, when calling the function createSquare to pass in an object created by the literal amount of the object, after additional attribute checking, it is found that the colooour attribute is not the attribute contained in the target type, and an error is reported.
Solution 1: use type assertion

let mySquare = createSquare({ colooour: "red", width: 100 } as SquareConfig);

Solution 2: assign another variable

let opt = { colooour: "red", width: 100 };
let mySquare = createSquare(opt);

Unlike literals, the incoming opt object does not undergo additional property checking.

The above two methods can bypass the inspection, and the better way is to
To add a string signature index to the interface SquareConfig:

interface SquareConfig {
    color?: string;
    width?: number;
    [propName: string]: any;
}

The premise is that you need to make sure that the object can have some extra properties, so that you can have any number of properties, and as long as it is not color and width, it doesn't matter what type it is.
You can use the above techniques to avoid checking when dealing with complex object structures, but try not to use them in simple code, but to modify the interface definition to support additional attribute passing in.

Function type

Interfaces can describe the various shapes of objects in JavaScript. In addition to describing common objects with properties, interfaces can also describe function types.
In order to use an interface to represent function types, we need to define a call signature for the interface.
Call signature is like a function definition with only parameter list and return value type.

interface SearchFunc {
    // Call signature
    (source: string, subString: string): boolean
}

This interface is a function type interface.
Let's use the interface of this function type.

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

The parameter name of the function does not need to match the name defined in the interface. However, it is required that the parameter type on the corresponding location is compatible, and the return value type should be consistent with the interface definition.

Indexable types

Similar to using interfaces to describe function types, we can also describe those types that can be "obtained through index", 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 signature can be divided into two types: Digital index signature and string index signature.
Digital index signature is to define an interface with digital index signature, and index it by number type to get a return value of specified type.

interface StringArray {
    // digital signature
    [index: number]: string
}

let myArray: StringArray = ['bob', 'fred', 'smith'];
let myStr: string = myArray[0];

String index signature is different from digital index signature only in index type,

interface StringArray {
    // String index signature
    [index: string]: string
}

let myArray: StringArray = { 'a': 'aaa', 'b': 'bbb' };
let myStr22: string = myArray22['a'];
console.log(myStr22) // aaa

These two are the two index formats supported by TS. it should be noted that the return value type of the number index must be a subtype of the return value type of the string index, because when accessing the number index, the number will be converted to a string.
Such as:

let myStr: string = myArray[0];
// Amount to
let myStr: string = myArray['0'];

Another example is:

class Animal {
    name: string
}

class Dog extends Animal {
    breed: string
}

interface NotOkay {
    [x: number]: Dog // digital signature
    [x: string]: Animal // String signature
    // The type returned by the digital signature is the subtype returned by the string signature index
}

let an1 = new Animal()
let do1 = new Dog()
let oo1: NotOkay = { 'bb': an1, 2: do1 }

console.log(oo1[2]) // When Dog {} accesses a digital signature, it converts the number to a string,
// That's why the return type of digital signature is a subtype of the return type of string signature
console.log(oo1['bb']) // Animal {}

In addition, string index signatures are good at describing dictionary patterns, and they also ensure that all properties match their return value types.
Such as:

interface NumberDictionary {
  [index: string]: number;
  length: number;    // Yes, length is of type number
  name: string       // Error, type of 'name' does not match type of return value of index type
}

Finally, you can set the index signature to read-only, which prevents the index from being assigned a value:

interface ReadonlyStringArray {
    readonly [index: number]: string;
}

Class type

A class in TS can also implement an interface so that the class has some kind of constraint or contract.

interface ClockInterface {
    currentTime: Date
    setTime(d: Date)
}

// Class implements an interface by having the properties and methods defined in the interface
class Clock2 implements ClockInterface {
    currentTime: Date
    constructor(h: number, m: number) {

    }

    setTime(d: Date) {
        this.currentTime = d
    }
}

Class has two types: the type of the static part and the type of the instance. The implementation interface is called instance type.
Class implements the interface. The interface only describes the public part of the class (that is, the instance type of the class). It does not check the private members of the class.
The properties and methods that implement the interface belong to the instance type of the class.
The constructor of a class is a static type.
Constructor interface
When an interface is defined with a constructor signature and a class is defined to implement the interface, an error will be generated. As mentioned above, the constructor exists in the static part of the class, and the class only performs type checking on the instance part.
The following is wrong:

interface ClockConstructor {
   // Constructor signature
   new(hour: number, minute: number)
}

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

How to use the constructor interface
We've learned about instance interfaces, constructor interfaces. We also know that a class cannot directly implement a constructor interface. So how do I use the constructor interface? See the following example:

// For instance method
interface ClockInterface {
    tick()
}.
// For constructors
interface ClockConstructor {
    // Constructor signature
    new(hour: number, minute: number): ClockInterface
}

// Checks if the first parameter matches the constructor signature, ClockConstructor
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
    return new ctor(hour, minute)
}

// Define two classes to implement instance interface
class DigitalClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log('tick  Digital');
    }
}

class AnalogClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log('tick  Analog');
    }
}

let digital = createClock(DigitalClock, 12, 17)
let anglog = createClock(AnalogClock, 7, 32)

Because the first parameter of createlock function is the type of ClockConstructor, when calling createlock, it will check whether DigitalClock and analoglock conform to the constructor signature. Because the DigitalClock and AnalogClock classes all implement the interface ClockInterface, and the ClockConstructor interface signature return value type is ClockInterface, then the DigitalClock and AnalogClock classes meet the conditions.

In this way, we implement a factory method that creates an instance with the type passed in, which is constrained by the constructor interface.

Inheritance interface

Interfaces can also inherit each other, copy members from one interface to another, and more flexibly divide interfaces into reusable modules. And can inherit multiple interfaces at the same time.

interface Shape {
    color: string
}

interface PenStroke {
    penWidth: number
}

interface Square extends Shape, PenStroke {
    sideLength: number
}

// An interface can inherit multiple interfaces and create a composite interface of multiple interfaces
let squre = {} as Square
squre.color = 'blue'
squre.sideLength = 10
squre.penWidth = 5.0

Mixed type

As we mentioned earlier, interfaces can describe rich types in JavaScript. Because JavaScript is dynamic and flexible, sometimes you want an object to have multiple types mentioned above at the same time.

interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}

function getCounter(): Counter {
    let counter = <Counter>function (start: number) { };
    counter.interval = 123;
    counter.reset = function () { };
    return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

In the example, the object returned by getCounter is of type Counter, which is not only a function type, but also an object type, with reset method and interval attribute.

*Interface inheritance class

Interface inherits classes, with few usage scenarios.
When an interface inherits a class, it inherits the members of the class, but does not include the implementation. It seems that the interface declares all the members of the class, but does not provide specific implementation, and will also inherit the private and protected success.
This means that when an interface inherits a class with private or protected members, the interface can only be implemented by the class or its subclass.
Take an example:

class Control {
    private state: any
}

interface SelectableControl extends Control {
    select()
}

// Declare the Button class and inherit the Control class, then implement the SelectableControl interface
class Button extends Control implements SelectableControl {
    select() { }
}

// This class implements the interface without inheriting Control, which is not possible. It lacks the private member state
// class ImageC implements SelectableControl {
//     select() { }
// }

The interface SelectableControl inherits the Control class, that is to say, the interface also inherits the member state in the class. Only the subclass of Control class can implement this interface.
In the above code, the Button class inherits the Control class, so it naturally inherits the member state in the class, and then implements the SelectableControl interface. The interface requires that there are state members in the class that implements it, and the Button satisfies, so this is correct.
The ImageC class implements the SelectableControl interface without inheriting the Control class. The interface checks that there is no member state in the class, so an error is reported.

finish

There are so many contents related to interface. Interface plays a very important role in TS and is widely used.

Posted by k994519 on Tue, 10 Dec 2019 02:45:43 -0800