Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

TypeScript interface


May 07, 2021 TypeScript


Table of contents


TypeScript interface

Introduced

One of typeScript's core principles is to type check the shape that the value has. I t is sometimes referred to as "duck-type" or "structural subtype". In TypeScript, interfaces are designed to name these types and define contracts for your code or third-party code.

Interface initial exploration

Here's a simple example to see how the interface works:

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

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

The type checker views printLabel printLabel an argument and requires this object argument to have a property named label string string. I t is important to note that the object parameters we pass in actually contain many properties, but the compiler only checks for the existence of those required properties and whether their types match. However, there are times when TypeScript isn't so loose, and we'll cover it a little bit below.

Let's override the example above, this time using an interface to describe that you must include a label property and the type string

interface LabelledValue {
  label: string;
}

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

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

LabelledValue interface is like a name that describes the requirements in the example above. I t represents an object with label property and a type string I t's important to note that we can't implement this interface here with objects that are printLabel do in other languages. W e're just going to look at the shape of the value. An incoming object is allowed as long as it meets the necessary conditions mentioned above.

It is also worth noting that the type inspector does not check the order of the properties, as long as the corresponding properties exist and the type is correct.

Optional properties

Not all of the properties in the interface are required. S ome exist only under certain conditions, or do not exist at all. Optional properties are commonly used when applying "option bags" mode, where only some properties are assigned to parameter objects passed in by functions.

Here's an example of applying "option bags":

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 properties are similar to normal interface definitions, with just one ? Symbol.

One of the benefits of optional properties is that you can predefine properties that may exist, and the second is that you can catch errors that refer to properties that do not exist. For example, if we deliberately misspelled the color property name in createSquare we'll get an error prompt: color

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

function createSquare(config: SquareConfig): { color: string; area: number } {
  let newSquare = {color: "white", area: 100};
  if (config.color) {
    // Error: Property 'collor' does not exist on type 'SquareConfig'
    newSquare.color = config.collor;  // Type-checker can catch the mistyped name here
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}

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

Read-only properties

Some object properties can only modify the value of an object when it was first created. You can specify read-only readonly before the property name:

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

You can construct a Point by assigning a literal amount of an Point After the assignment, x and y longer be changed.

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

TypeScript has ReadonlyArray<T> which is similar to Array<T> it just removed all the variable methods, so you can make sure that the array can no longer 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!

On the last line of the code above, you can see that even assigning the entire ReadonlyArray to a normal array is not okay. But you can override with type assertions:

a = ro as number[];

readonly vs const

The easiest way to readonly or const is to use it as a variable or as a property. const if used as a variable and readonly

Additional property checks

We used the interface in the first example, typeScript let's pass in { size: number; label: string; } expect only the { label: string; } function of the . We've learned about optional properties and know they're useful in "option bags" mode.

However, naively combining the two would be like lifting a stone and throwing a stone at your own feet, as in JavaScript. For example, createSquare

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

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

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

Note that the createSquare are spelled as colour instead of color In JavaScript, this fails silently.

You might argue that the program has been typed correctly because width is compatible, color property, and the extra colour property is meaningless.

However, TypeScript will think that there may be a bug in this code. O bject literals are treated specially and subject to additional property checks when they are assigned to variables or passed as arguments. If an object literally has any properties that are not included in the Target Type, you get an error.

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

It's easy to get around these checks. The easiest way is to use type assertions:

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 are able to determine that the object may have some additional properties for special purpose use. If SquareConfig the color and width properties color width above, and also has any number of other properties, we can define it this way:

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

We'll talk about index signatures later, but what we're saying here SquareConfig have any number of properties, and as long as color and width it doesn't matter what their type is.

There's one last way to skip these checks, which might surprise you by assigning this object to another squareOptions compiler doesn't report errors.

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

Keep in mind that in simple code like the one above, you probably shouldn't go around these checks. Y ou may need to use these techniques literally for complex objects that contain methods and internal states, but most additional property check errors are real bugs. T hat means you've encountered additional type check errors, such as selecting a package, and you should review your type declaration. Here, if you support passing color or colour createSquare you should modify the SquareConfig definition to reflect this.

The type of function

Interfaces can describe the various shapes that objects in JavaScript have. In addition to describing normal 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 call signature for the interface. I t's like a function definition with only a list of parameters and a return value type. Each argument in the argument list requires a name and type.

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

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

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

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

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

The parameters of the function are checked one by one, requiring that the parameter types at the corresponding locations be compatible. I f you don't want to specify a type, Typescript's type system infers the argument type because the function is assigned directly SearchFunc type variable. T he return value type of a function is inferred from its return value (in this false and true If you let this function return a number or string, the type inspector warns us that the return value type of the function does not match the definition in the SearchFunc interface.

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

The type that can be indexed

Similar to using interfaces to describe function types, we can also describe types that can be "indexed", such as a[10] ageMap["daniel"] T he indexable type has an index signature that describes the type of object index and the corresponding index return value 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 example above, we defined StringArray interface, which has an index signature. This index signature indicates that when you index string StringArray get a return value of string type. number

There are two types of index signatures supported: strings and numbers. B oth types of indexes can be used at the same time, but the return value of the numeric index must be a sub-type of the string index return value type. T his is because number use number to index, JavaScript converts it to string and string goes to index the object. That 100 number is "100" string so the two need to be consistent.

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

// Error: indexing with a 'string' will sometimes get you a Dog!
interface NotOkay {
    [x: number]: Animal;
    [x: string]: Dog;
}

String index signatures are a good description of dictionary pattern, and they also ensure that all properties match the type of value they return. B ecause the string index obj.property and obj["property"] are available. In the following example, name does not match the type of string index, so the type inspector gives an error prompt:

interface NumberDictionary {
  [index: string]: number;
  length: number;    // 可以,length是number类型
  name: string       // 错误,`name`的类型不是索引类型的子类型
}

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;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!

You myArray[2] the index signature is read-only.

Class type

Implement the interface

TypeScript can also be used to explicitly force a class to conform to a contract, as is the basic function of an interface in C# or Java.

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, just like setTime method below:

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

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

Interfaces describe the public part of a class, not the public and private parts. It does not help you check if the class has some private members.

The difference between the static part of the class and the instance part

When you manipulate classes and interfaces, you need to know that classes have two types: the type of static part and the type of instance. You'll notice that when you define an interface with a constructor signature and try to define a class to implement the 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. Constructor exists in the static part of the class, so it is not within the scope of the check.

Therefore, we should directly manipulate the static part of the class. L ooking at the following example, we define two ClockConstructor for constructors and ClockInterface for instance methods. To make it easier for us to define a createClock which creates instances from incoming types.

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 the first argument of createClock is the ClockConstructor createClock(AnalogClock, 7, 32) AnalogClock is checked for conformity with constructor signatures.

Extend the interface

Like classes, interfaces can scale to each other. This allows us to copy members from one interface to another, giving us more flexibility to split 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 and create a composite interface for 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;

Hybrid 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.

An example is an object that can be used as both a function and 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 define the type in its entirety, as above.

The interface inherits the class

When an interface inherits a class type, it inherits the members of the class but does not include its implementation. I t is as if the interface declares the members that exist in all classes, but does not provide a specific implementation. I nterfaces also inherit private and protected members of the class. This means that when you create an interface that inherits a class that has private or protected members, the interface type can only be implemented by that class or its sub-classes.

This is useful when you have a very deep inheritance, but just want your code to work only for sub-classes that have specific properties. S ub-classes have no connection to the base class except 10 inherited from the base class. Cases:

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, SelectableControl all members of Control including the private state B ecause state is a private member, selectableControl Control can only be implemented by SelectableControl Because only Control sub-classes can have a Control on state this is required for the compatibility of private members.

Within Control class, private member state is allowed to be accessed through an instance of SelectableControl state I n fact, SelectableControl is like Control and has a select method. Button and TextBox SelectableControl (because they both Control and have select select but Image and Location are not.