ts study notes -- interface

ts - interface

introduce

One of the core principles of TypeScript is type checking the structure a value has . It is sometimes called "duck typing" or "structural subtyping". In TypeScript, the role of interfaces is to name these types and define contracts for your code or third-party code.

A Preliminary Study on the Interface

Let's use a simple example to observe how the interface works:

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

let myObj = {
    
     size: 10, label: "Size 10 Object" };
printLabel(myObj);

Calls that the type checker looks at printLabel. printLabelhas a parameter and requires that this object parameter has a property named labeltype string. It should be noted that the object parameter we pass in will actually contain many properties, but the compiler will only check that those required properties exist and their types match. However, sometimes TypeScript is not so relaxed, we will explain it a little bit below.

Let's rewrite the above example, this time using the interface to describe: must contain a labelproperty and the type is string:

interface LabelledValue {
    
    
  label: string;
}

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

let myObj = {
    
    size: 10, label: "Size 10 Object"};
printLabel(myObj);

LabelledValueAn interface is like a name used to describe the requirements in the above example. It represents labelan stringobject with a property of type . It should be noted that here we cannot say that the object printLabelpassed . We will only care about the shape of the value. As long as the passed-in object satisfies the necessary conditions mentioned above, then it is allowed.

It is also worth mentioning that the type checker does not check the order of the attributes, as long as the corresponding attribute exists and the type is correct.

optional attributes

Not all properties in an interface are required. Some are only present under certain conditions, or not at all. Optional attributes are often used when applying the "option bags" pattern, that is, only some of the attributes in the parameter object passed to the function are assigned values.

Here is an example with "option bags" applied:

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

function createSquare(config: SquareConfig): {
    
    color: string; area: number} {
    
    
  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"});

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

One of the benefits of optional attributes is that the attributes that may exist can be predefined, and the second advantage is that errors when referencing attributes that do not exist can be caught. For example, if we createSquaredeliberately colormisspell the attribute name in , we will get an error message:

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

function createSquare(config: SquareConfig): {
    
     color: string; area: number } {
    
    
  let newSquare = {
    
    color: "white", area: 100};
  if (config.clor) {
    
    
    // Error: Property 'clor' does not exist on type 'SquareConfig'
    newSquare.color = config.clor;
  }
  if (config.width) {
    
    
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}

let mySquare = createSquare({
    
    color: "black"});

read-only attribute

Some object properties can only change their values ​​when the object is first created. You can specify read-only properties readonlywith :

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

You can construct one by assigning an object literal Point. After assignment, xand ycan no longer be changed.

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

TypeScript has ReadonlyArray<T>types, which are Array<T>similar to , except that all variable methods are removed, so you can ensure that the array cannot be modified after it is created:

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

In the last line of the above code, you can see that it ReadonlyArrayis not possible to assign the entire value to an ordinary array. But you can rewrite it with a type assertion:

a = ro as number[];

readonly vs const

The easiest way to judge whether to use readonlyor not constis to see whether to use it as a variable or as an attribute. Use it if it is used as a variable const, and use it if it is used as an attribute readonly.

Additional property checks

We used interfaces in the first example, and TypeScript lets us pass { size: number; label: string; }in only the functions we expect { label: string; }. We've already looked at optional attributes and know that they are useful in the "option bags" pattern.

However, naively combining the two is like shooting yourself in the foot in JavaScript. For instance, take for createSquareexample :

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

function createSquare(config: SquareConfig): {
    
     color: string; area: number } {
    
    
    // ...
}

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

Note that createSquarethe parameters passed in are spelled * colourinstead of color. In JavaScript, this would fail silently.

You could argue that this program is already correctly typed because widththe attributes are compatible, there are no colorattributes, and the extra colourattributes are pointless.

However, TypeScript will consider this code to be potentially buggy. Object literals are treated specially and subject to extra property checks when assigning them to variables or passing them as parameters. If an object literal has any properties that the "target type" does not contain, you will get an error.

// error: 'colour' not expected in type 'SquareConfig'
let mySquare = createSquare({
    
     colour: "red", width: 100 });

Bypassing these checks is very simple. The easiest way is to use a type assertion:

let mySquare = createSquare({
    
     width: 100, opacity: 0.5 } as SquareConfig);

However, the best way is to be able to add a string index signature, provided that you can determine that this object may have some extra properties for special purposes. If SquareConfighas a colorsum widthproperty of the type defined above, and any number of other properties, we can define it like this:

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

We'll get to index signatures later, but what we're saying here is that there SquareConfigcan be any number of properties, and as long as they're not colorsums width, then it doesn't matter what their type is.

There is one last way of skipping these checks, which may surprise you, and it is to assign this object to another variable: since the extra property check squareOptionsis not , the compiler will not report an error.

let squareOptions = {
    
     colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);

Note that in simple code like the above, you probably shouldn't bypass these checks. For complex object literals that contain methods and internal state, you may need to use these tricks, but most extra property checking errors are real bugs. That is, if you encounter errors detected by additional type checking, such as "option bags", you should review your type declarations. Here, if passing in coloror colourattributes to is supported createSquare, you should modify SquareConfigthe definition to reflect this.

function type

Interfaces can describe the various shapes that objects in JavaScript can have. In addition to describing ordinary objects with properties, interfaces can also describe function types.

In order to use an interface to represent a function type, we need to define a calling signature for the interface. It is like a function definition with only parameter list and return type. Each parameter in the parameter list requires a name and a type.

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

After this definition, we can use this function type interface like any other interface. The following example shows how to create a variable of function type and assign a function of the same type to this variable.

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
    
    
  let result = source.search(subString);
  return result > -1;
}

For type checking of function types, the parameter names of the function do not need to match the names defined in the interface. For example, let's rewrite the above example using the following code:

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

The parameters of the function will be checked one by one, and the parameter types at the corresponding positions are required to be compatible. If you don't want to specify the type, TypeScript's type system will infer the parameter type, because the function is directly assigned to SearchFuncthe type variable. The return type of a function is inferred from its return value ( falseand true). If we let this function return a number or a string, the type checker will warn us that the return type of the function does not match the definition in SearchFuncthe interface .

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

indexable type

Similar to using interfaces to describe function types, we can also describe types that can be "indexed", such as a[10]or ageMap["daniel"]. An indexable type has an index signature , which describes the type of object index, and the corresponding index return type. Let's look at an example:

interface StringArray {
    
    
  [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

In the above example, we defined StringArraythe interface, which has an index signature. This index signature numberindicates the type of return value StringArraythat will be returned when used to index string.

TypeScript supports two types of index signatures: strings and numbers. Both types of indexes can be used at the same time, but the return value of the numeric index must be a subtype of the return value type of the string index. This is because when using numberto index, JavaScript will convert it to stringand then index the object. That is to say, using 100(one number) to index is equivalent to using "100"(one string) to index, so the two need to be consistent.

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

// 错误:使用数值型的字符串索引,有时会得到完全不同的Animal!
interface NotOkay {
    
    
    [x: number]: Animal;
    [x: string]: Dog;
}

String index signatures describe the schema well dictionary, and they also ensure that all properties match their return type. Because the string index declares obj.propertyand obj["property"]both forms are fine. In the following example, namethe type of does not match the string index type, so the type checker gives an error:

interface NumberDictionary {
    
    
  [index: string]: number;
  length: number;    // 可以,length是number类型
  name: string       // 错误,`name`的类型与索引类型返回值的类型不匹配
}

Finally, you can set the index signature to be read-only, which prevents assignment to the index:

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

You can't set that myArray[2]because index signatures are read-only.

class type

implement the interface

Similar to the basic role of interfaces in C# or Java, TypeScript can also use it to explicitly enforce a class to conform to a certain contract.

interface ClockInterface {
    
    
    currentTime: Date;
}

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

You can also describe a method in an interface and implement it in a class, as in the following setTimemethod:

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

class Clock implements ClockInterface {
    
    
    currentTime: Date;
    setTime(d: Date) {
    
    
        this.currentTime = d;
    }
    constructor(h: number, m: number) {
    
     }
}

An interface describes the public part of a class, not both public and private parts. It won't help you check if a class has some private members.

Difference between class static part and instance part

When you work with classes and interfaces, you need to know that classes have two types: the type of the static part and the type of the instance. You'll notice that when you define an interface with a constructor signature and try to define a class that implements that interface, you get an error:

interface ClockConstructor {
    
    
    new (hour: number, minute: number);
}

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

This is because when a class implements an interface, only its instance part is type checked. The constructor exists in the static part of the class, so it is not within the scope of the inspection.

Therefore, we should directly manipulate the static part of the class. Looking at the example below, we define two interfaces, one ClockConstructorfor constructors and ClockInterfaceone for instance methods. For convenience we define a constructor createClockthat creates an instance with the type passed in.

interface ClockConstructor {
    
    
    new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
    
    
    tick();
}

function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
    
    
    return new ctor(hour, minute);
}

class DigitalClock implements ClockInterface {
    
    
    constructor(h: number, m: number) {
    
     }
    tick() {
    
    
        console.log("beep beep");
    }
}
class AnalogClock implements ClockInterface {
    
    
    constructor(h: number, m: number) {
    
     }
    tick() {
    
    
        console.log("tick tock");
    }
}

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

Because createClockthe first parameter of is ClockConstructorthe type, in createClock(AnalogClock, 7, 32)it, it will check AnalogClockwhether it conforms to the constructor signature.

inherit interface

Like classes, interfaces can also inherit from each other. This allows us to copy members from one interface to another, allowing for more flexibility in splitting interfaces into reusable modules.

interface Shape {
    
    
    color: string;
}

interface Square extends Shape {
    
    
    sideLength: number;
}

let square = <Square>{
    
    };
square.color = "blue";
square.sideLength = 10;

An interface can inherit multiple interfaces to create a composite interface of multiple interfaces.

interface Shape {
    
    
    color: string;
}

interface PenStroke {
    
    
    penWidth: number;
}

interface Square extends Shape, PenStroke {
    
    
    sideLength: number;
}

let square = <Square>{
    
    };
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

mixed type

We mentioned earlier that interfaces can describe the rich types in JavaScript. Because of JavaScript's dynamic and flexible characteristics, sometimes you will want an object to have multiple types mentioned above at the same time.

An example is that an object can be used both as a function and as an object, with additional properties.

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;

When using JavaScript third-party libraries, you may need to fully define the type as above.

interface inheritance class

When an interface inherits from a class type, it inherits the members of the class but not its implementation. It's as if the interface declares all the members that exist in the class, but does not provide the concrete implementation. Interfaces also inherit private and protected members of the class. This means that when you create an interface extending a class that has private or protected members, the interface type can only be implemented by that class or its subclasses.

This is useful when you have a large inheritance structure, but the point is that your code only works if the subclass has a certain property. This subclass has no relationship to the base class other than inheriting from it. example:

class Control {
    
    
    private state: any;
}

interface SelectableControl extends Control {
    
    
    select(): void;
}

class Button extends Control implements SelectableControl {
    
    
    select() {
    
     }
}

class TextBox extends Control {
    
    
    select() {
    
     }
}

// 错误:“Image”类型缺少“state”属性。
class Image implements SelectableControl {
    
    
    select() {
    
     }
}

class Location {
    
    

}

In the example above, all members of the SelectableControlare included , including private members . Because it is a private member, only subclasses of it can implement the interface. This is necessary for private member compatibility because only subclasses of can have a private member declared on.ControlstatestateControlSelectableControlControlControlstate

Inside the class, private members Controlare allowed to be accessed through the instance of the class . In fact, an interface is the same as a class with methods . The and classes are subclasses of (because they both inherit from and have methods), but the and classes are not.SelectableControlstateSelectableControlselectControlButtonTextBoxSelectableControlControlselectImageLocation

Guess you like

Origin blog.csdn.net/weixin_44801790/article/details/126345903