TypeScript – Interfaces

Directory

1. Interface Introduction

1.1 Interface Example

2. Optional attributes

3. Read-only attribute

4. Additional attribute checks

5. Function type

6. Indexable types

7. Class type

7.1 Class static part and instance part

8. Inheritance interface

9. Hybrid type

10. Interface inheritance class


1. Interface introduction

One of the core principles of TypeScript is type checking the _structure_ that values have. 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.

1.1 Interface Example

Interface declaration is another way to name object types, see the following example:

function people(obj: { name: string, age: number }) {
  console.log(obj.name + 'age is:' + obj.age);
}
let obj = { age: 20, name: 'Zhang San', addr: 'Beijing' };
people(obj)

// Zhang San's age is: 20

In the people method here, an object can be passed in, and the object must have a name attribute and an age attribute, that is, when passing in parameters, if an object is passed directly, then this object must be the parent of the method declaration object The set is the attribute of the object in the method people, which can be found in the incoming object, and the corresponding attribute types must be consistent.

Look at another way of writing:

function people(obj: { name: string, age: number }) {
  console.log(obj.name + 'age is:' + obj.age);
}
people({age:20, name: 'z', addr: 'Beijing'})

// Object literals can only specify known properties, and "addr" is not in the type "{ name: string; age: number; }".

Of course, we define interfaces in advance, or type aliases are also possible, as follows:

interface MyInterface {
  name: string,
  age: number
}
type MyType = {
  name: any,
  age: number,
  addr: 'Beijing'
}

function people(obj: MyInterface | MyType) {
  console.log(obj.name + 'age is:' + obj.age);
}
let myObj = {age:20, name: 'Zhang San', addr: 'Beijing'}
people(myObj)

// Zhang San's age is: 20

As long as MyInterface and Mytype are both name and age attributes, because the checker will check whether each attribute exists and their corresponding types one by one, the type must be less than or equal to the defined type. Unlike other object-oriented languages, such as Java, which must implement a certain interface like them, they are compiled languages, whether there will be strict restrictions on compilation, JavaScript is an interpreted language, and the restrictions are not so Strict, and TypeScript is a superset of JavaScript, so it naturally follows this rule, as long as the incoming interface or type alias has corresponding properties, and this is why TypeScript is sometimes called “duck typing”.

The duck type comes from James Whitecomb Riley’s famous quote: “Anyone who walks like a duck and quacks is a duck.”

2. 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:

interface MyInterface {
  name: string,
  age: number
  addr?: string
}

function people(obj: MyInterface) {
  console.log(obj.name + 'age is:' + obj.age);
}
let myObj = {age: 20, name: 'Zhang San'}
people(myObj)

An optional attribute is to add a question mark (?) after the attribute, so the attributes with a question mark are all non-required attributes.

The advantage of optional attributes is that they can be pre-defined. Later, you can judge whether the attribute exists according to the actual situation. Of course, this method has another advantage. When writing code, you can automatically perform intelligent prompts to find the corresponding attributes.

As follows:

This also prevents us from writing wrong attributes without being discovered.

3. Read-only attribute

When declaring an object property, you can add readonly before the property name to specify that it is a read-only property.

interface People = {
    readonly name: string,
    readonly age: number
}
let p1:People = {name: 'Zhang San', age: 12}
p1.age = 24; // compile error cannot assign a value to "age" because it is a read-only property. 

After the object is rebuilt, all properties cannot be changed.

The ReadonlyArray type can ensure that the array cannot be changed after it is created. The readable array type is actually added after ES2019 (ES10), as shown below:

let numArr:number[] = [10, 30, 50, 70, 90];
let readonlyArr:ReadonlyArray<number> = numArr;
readonlyArr[0] = 123; // Index signatures in type "readonly number[]" only allow reading.
readonlyArr.push(123) // Property 'push' does not exist on type 'readonly number[]'
readonlyArr.splice(1); // Property 'splice' does not exist on type 'readonly number[]'
readonlyArr.length = 1 // Cannot assign a value to 'length' because it is a read-only property.
numArr = readonlyArr // not assignable to mutable type "number[]"

4. Additional attribute check

Optional attributes are specified in the object. If an object literal is passed in, and the attributes in the object literal are inconsistent with the attributes defined in the previous object, an error will be reported when compiling, saying that the object literal can only specify known properties.

For example: when the attribute name is written as names, it is as follows:

interface MyInterface {
    name?: string,
    age?: number
    addr?: string
  }
  
  function people(obj: MyInterface) {
    console.log(obj.name + 'age is:' + obj.age);
  }
  people({age: 20, names: 'Zhang San'})
 // A parameter of type "{ age: number; names: string; }" is not assignable to a parameter of type "MyInterface". 

If the compilation check is skipped, there are two ways:

 // The first way, use type assertion
  people({age: 20, names: 'Zhang San'} as MyInterface)
  // The second way, assign the literal object to a variable.
  let obj = {age: 20, names: 'Zhang San'}
  people(obj)

The second way, although it can jump to compile check, may cause bugs. Although it can run, the result will be different from the expected one. Therefore, it is best to avoid it in advance and find the corresponding bugs during compilation.

If we determine that the object may have multiple unknown properties, we can add a string index signature, similar to JavaScript functions, passing parameters (…obj), indicating that the function may receive multiple other parameters.

interface MyInterface {
    name?: string,
    age?: number,
    addr?: string,
    [other:string]: any
}

5. Function type

We can use the interface to represent the function type. The parameter attribute is the same as the previous object attribute definition, and the return type of the function is added at the end, as shown below:

interface MyInterface {
    (name: string, age: number, addr?: string) : string
  }
  
let p1:MyInterface;
p1 = function(name:string, age:number, addr?:string) {
    return name + 'age is:' + age
}
console.log('p1: ', p1("Zhang San", 20));

The definition of object function parameters is the same as in JavaScript. The name does not have to be the same as that defined in the interface. The parameters in the function are related to the order of passing parameters, so here, the actual value passed and the order of function parameters can be matched.

If the parameter type is not specified in the function, it will be automatically inferred according to the interface definition.

interface MyInterface {
    (name: string, age: number, addr?: string) : string
  }
  
let p1:MyInterface;
p1 = function(a, b, addr) {
    return a + 'Age is:' + b
}
console.log('p1: ', p1("Zhang San", 20));

6. Indexable types

We can use the indexed access type to look up a specific property on another type:

type Person = { age: number; name: string; alive: boolean };
let p1: Person;
p1= {age: 10, name: 'Zhang San', alive: true}
console.log('Person["age"]: ', p1["age"]);
// Person["age"]: 10 

The index type is itself a type, so we can use unions, or other types of types entirely: keyof

type Person = { age: number; name: string; alive: boolean };

// type I1 = string | number
type I1 = Person["age" | "name"];

// type I2 = string | number | boolean
 
type I2 = Person[keyof Person];
     
type AliveOrName = "alive" | "name";
// type I3 = string | boolean
type I3 = Person[AliveOrName];

You’ll even see an error if you try to index a property that doesn’t exist:

type Person = { age: number; name: string; alive: boolean };

// Compile error, property "alve" does not exist on type "Person".
type I1 = Person["alve"];

Used to get the type of array element, we can conveniently capture the element type corresponding to the array: number typeof

const MyArray = [
    { name: "Alice", age: 15 },
    { name: "Bob", age: 23 },
    { name: "Eve", age: 38 },
  ];
   
// type Person = {
// name: string;
// age: number;
// }
type Person = typeof MyArray[number];

// type Age = numbers
type Age = typeof MyArray[number]["age"];
// Or
type Age2 = Person["age"];

Types can only be used at index time, which means variable references cannot be made using: const

const key = "age";
//An error is reported, the type "key" cannot be used as an index type.
type Age = Person[key];
// Can
type Age1 = Person['age'];

It is possible to use type aliases.

type key = "age";
// can type Age = number
type Age = Person[key];

7. Class type

The basic function of the interface in C# or Java is the same, and TypeScript can also use it to explicitly force a class to conform to a certain contract. That is to face the concept of class inheritance in object programming, define an interface, and let the class implement this interface.

interface animal {
    birthday: Date
    getInfo(): string
}

class Cat implements animal {
    birthday: Date;
    name: string;
    getInfo() : string {
        return this.name + ", " + this.birthday;
    }
    constructor(name: string, birthday: Date) {
        this.name = name;
        this. birthday = birthday;
    }
}

 let c1 = new Cat('jingle cat', new Date('2023-05-09'));
 console.log(c1.getInfo());
 // Jingle cat, Tue May 09 2023 08:00:00 GMT + 0800 (China Standard Time)

7.1 Class static part and instance part

When working with classes and interfaces, when you define an interface with a constructor signature and try to define a class that implements the interface, you get an error:

interface Animal {
    birthday: Date
    new(name: string, birthday: Date)
}
// Compile error, type 'Cat' provided does not match signature 'new (name: string, birthday: Date): any'.
class Cat implements Animal {
    birthday: Date;
    name: string;
    getInfo() : string {
        return this.name + ", " + this.birthday;
    }
    constructor(name: string, birthday: Date) {
        this.name = name;
        this. birthday = birthday;
    }
}

Therefore, we make some changes in the static part of the class. We add another interface AnimalInter, then add a return value of the original interface constructor to the AnimalInter interface, and create a new function. The return type of the function is the AnimalInter interface, and the first function Parameters are instantiated, as shown in the following figure:

interface Animal {
    new(name: string, birthday: Date): AnimalInter
}

interface AnimalInter {
    getInfo() : string
}

function CreateFunc(first:Animal, name: string, birthday: Date ):AnimalInter {
    return new first(name, birthday)
}

class Cat{
    birthday: Date;
    name: string;
    getInfo() {
        return this.name + ", " + this.birthday;
    }
    constructor(name: string, birthday: Date) {
        this.name = name;
        this. birthday = birthday;
    }
}

let result = CreateFunc(Cat, 'Zhang San', new Date('2023-02-09'))
console.log('result: ', result.getInfo());

In this way, when instantiating the Cat class, check whether it is consistent with the definition of the Animal interface constructor.

8. Inheritance interface

The inheritance of interfaces is the same as that of classes, and they can inherit from each other. In this way, multiple interfaces can be split to facilitate future reusability and refactoring, as shown below:

interface Animal {
    name: string,
}

interface AnimalInter extends Animal {
    getInfo() : string
}

class Cat implements AnimalInter{
    birthday: Date;
    name: string;
    getInfo() {
        return this.name + ", " + this.birthday;
    }
    constructor(name: string, birthday: Date) {
        this.name = name;
        this. birthday = birthday;
    }
}

let result = new Cat('Zhang San', new Date('2023-02-09'))
console.log('result: ', result.getInfo());

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

interface Animal {
    name: string,
}
interface AnimalSecond {
    age: number,
}

interface AnimalInter extends Animal, AnimalSecond {
    getInfo() : string
}

class Cat implements AnimalInter{
    birthday: Date;
    name: string;
    age: number;
    getInfo() {
        return this.name + ", " + this.birthday + ", age: " + this.age;
    }
    constructor(name: string, birthday: Date, age: number) {
        this.name = name;
        this. birthday = birthday;
        this. age = age;
    }
}

let result = new Cat('Zhang San', new Date('2023-02-09'), 20)
console.log('result: ', result.getInfo());

9. Mixed type

Mixed types, a variable may have multiple types, number | string | boolean, etc., an object may also be, the following is an object, and it can also be a function.

interface Animal {
    (age: number): string;
    name: string;
    time: number;
}

function getAnimal (): Animal {
    let animal = function(age: number) {} as Animal
    return animal;
}
let getResult = getAnimal();
getResult(20);
getResult.time = 20;
// Error, cannot assign a value to "name" because it is a read-only property. The name of the function, which is readable
getResult.name = 'Zhang San';
console.log(getResult.time);
console.log(getResult.name); 

10. Interface inheritance class

When an interface inherits a class, it inherits the members of the class, excluding the corresponding implementation. Interfaces can inherit private members and protected members of a class. If you need to inherit this interface, it can only be inherited by subclasses of the interface inheritance class. In the constructor function, the constructor of the subclass must contain a call to “super”.

class AnimalClass {
   private name: any;
}

interface AnimalInterface extends AnimalClass{
    getName(): string;
}

class Animal extends AnimalClass implements AnimalInterface{
    getName() {
        return '123';
    };
    constructor() {
       super();
    }
}

// Property "name" is missing from type "Cat", but required from type "AnimalInterface".
class Cat implements AnimalInterface {
    getName() {
        return ''
    }
}

let anima = new Animal()

The Animal class inherits the AnimalClass class. In the constructor, the super() method must be called. When the subclass is initialized, the member properties defined in the parent class also need to be initialized, and the AnimalInterface interface can only be used by subclasses of the AnimalClass class. Implementation, because the interface itself inherits the AnimalClass class, and when Cat implements the AnimalInterface interface, it must include all the properties inherited from other classes, including private properties. The Cat class is not a subclass of the AnimalInterface interface inheritance class, so it reports an error , and the private properties of the parent class cannot be directly accessed through the object instantiated by the subclass, nor will they be inherited.