Interface, class and generic type of "quick start TypeScript"

[TOC]

1, Interface

1. Definition of interface

In other languages, such as c++, java, python and other language interfaces, we will spend a lot of time learning, but in TS, we don't need to spend too much time on the interface. It is not as difficult to understand and use as the interface in the above language. When learning the interface in TS, you can think of "this interface is not the other interface". In short, the class in TS is used to describe a type

Let's take a look at the definitions in the code:

To define an interface, you need to use the keyword: interface

//Define interface type
interface Employee {
  name: string,
  salary: number
}

This is a simple interface

Let's see how to use this interface!

const em1:Employee =  {
    name:'jhon',
    salary: 123,
}

const em2: Employee = {
    name: 'ice_moss',
    salary: 8000,
}

console.log(em2, em1)

Output results:

{
  "name": "ice_moss",
  "salary": 8000,
},  {
  "name": "jhon",
  "salary": 12000
} 

We define an interface Employee, and then define variables em1 and em2. Their types are Employee. We use em1 and em2 to implement Employee's properties and methods. We call em1 and em2 to implement Employee's interface

2. Interface syntax

In the above example, we only describe the properties in the interface. Next, we implement the properties and methods together:

//Define interface type
interface Employee {
    name: string   
    salary: number
    bonus?: number
    getIncom():number  
}

const em1:Employee =  {
    name:'jhon',
    salary: 12000,
    bonus: 2000,
    getIncom(){
        return this.salary
    }
}

const em2: Employee = {
    name: 'ice_moss',
    salary: 8000,
    bonus: 3000,
    getIncom(){
        return this.salary + (this.bonus || 0)
    }
}
3. Advanced usage of interface
3.1 optional parameter series
interface Employee {
  name?:{     //Optional parameters
    first?: string  //Optional parameters
    last: string
  }
  salary: number
  bonus?: number
}

//Find optional parameters
function hasBadName(e: Emplotee){
  if(name){
    if(name.first){
      name.fisrt.startWith('AAA')
    }
  }
}

It looks more complicated. We have a simple method:

function hasBadName(e: Employee){
    return e.name?.first?.startsWith('AAA')
}
//Explanation: the first step is to judge whether e.name exists. If it does not exist, it returns undefined; If e.name exists, continue to judge downward in this way  

Let's look at the specific usage:

interface Employee {
    name?: {
        last?: string,
        first?: string,
    }   
    salary: number
    bonus?: number        
}

function hasBadName(e: Employee){
    return e.name?.first?.startsWith('AAA')
}

console.log(hasBadName({
    name:{
        first: 'jhon',
        last: 'smith',
    },
    salary: 8000,
}))

//Output: false

console.log(hasBadName({
    name:{
        last: 'smith',
    },
    salary: 8000,
}))

//Output: undefined

So we can protect our program from hanging up

3.2 non null assertion

In the optional parameter concatenation, we can't fundamentally solve the problem, so we need to use! Non null assertion

You only need to change the "3.1" to "3.1"? Here you are! Just.

3.3 interface expansion

We can understand it as inheritance in object-oriented language. The old interface can be understood as the father interface. The new interface is called the son interface. The son interface inherits the properties and methods of the father interface through the keyword extends

example:

//Son interface
interface Employee extends HasName {   //Interface expansion: Extensions
    salary: number
    bonus?: number       
}

//Father interface
interface HasName{
    name: {
        last: string,
        first: string,
    } 
}

//em1 implements the interface Employee
const em1: Employee = {
    name:{
        first:'_moss',
        last: 'ice'
    },
    salary: 20000,
    bonus: 1000
}

console.log(em1)

Output results:

{
  "name": {
    "first": "_moss",
    "last": "ice"
  },
  "salary": 20000,
  "bonus": 1000
} 
3.4 interface and type assertion
3.4.1 interface

The "union" here is actually more like the "intersection" in our mathematics

//Interface and type assertions
//Suppose this is a button
interface WxButton{
    visible: boolean,
    enabled:boolean,
    onClick():void,
}

//Suppose this is a picture
interface WxImage{
    visible: boolean,
    src: string,
    withd:number,
    heigth: number,
}

It can be seen that the "and" here actually takes the public parts of the two interface types

3.4.2 type assertion
//Interface and type assertions
interface WxButton{
    visible: boolean,
    enabled:boolean,
    onClick():void,
}

interface WxImage{
    visible: boolean,
    src: string,
    withd:number,
    heigth: number,
}

//Type Asserts 
function processElement(e: WxButton | WxImage){
    if((e as any).onClick){
        const btn = e as WxButton
        btn.onClick()
    }else{
        const img = e as WxImage
        console.log(img.src)
    }
}


processElement({
    visible: true,
    src: 'this is src',
    withd: 20,
    heigth: 30,
})

processElement({
    visible: true,
    enabled: true,
    onClick(){
        console.log('onClick')
    }
})

//Output results:
//  "this is src" 
//  "onClick"

2, Class

1. Definition of class

Class describes the common properties and methods of the created object.

TypeScript supports all object-oriented features, such as classes, interfaces, and so on.

The TypeScript class is defined as follows:

class class_name {
  //Scope
}

Class has three important modules:

  1. Properties:

    A field is a variable declared by a class.

  2. Constructor:

    Called when a class is instantiated, memory can be allocated for the object of the class.

  3. method:

    Method is the operation to be performed by the object.

1.1 properties (fields) of class
class Employee {
  name: string = 'ice_moss' //Note that class fields need to be initialized
  salary: number  = 20000
}
1.2 constructor

We declare an employee class. The constructor initializes the field name and salary of the class in the instance. During initialization, the constructor uses the keyword this: to represent the field under the class

class Employee {
  name: string = 'ice_moss' 
  salary: number  = 20000
  constructor(name: string, salary: number){
    this.name = name
    this.salary = salary
  }
}

//Of course, we can use the keyword public in the constructor, and then we don't have to declare fields separately:
class Employee {
  constructor(public name: string, public salary: number){
    this.name = name
    this.salary = salary
  }
//It will look more concise
//In fact, it can also be written directly as:
  class Employee {
  constructor(public name: string, public salary: number){}
1.3 method
class Employee {
  constructor(public name: string, public salary: number){
    this.name = name
    this.salary = salary
  }
  getName():void{  //Get name
      console.log(this.name)
  }
  getSalary():void{  //Get salary
      console.log(this.salary)
  }
}
2. Class instantiation
var object_name = new class_name([ arguments ])

It is still the Employee class

class Employee {
  constructor(public name: string, public salary: number){
    this.name = name
    this.salary = salary
  }
  getName():void{  //Get name
      console.log(this.name)
  }
  getSalary():void{  //Get salary
      console.log(this.salary)
  }
}

//Class, you need to use the keyword "new"
const em = new Employee('jhon', 9000)
console.log(em)
em.getName()
em.getSalary()


//Output results:
Employee: {
  "name": "jhon",
  "salary": 9000
} 
 "jhon" 
 9000 
3. Access control modifier

In TypeScript, access control characters can be used to protect access to classes, variables, methods, and constructor methods. TypeScript supports 3 different access permissions.

Public (default): public, which can be accessed anywhere.

Protected: protected and accessible by itself and its subclasses and parents.

Private: private, which can only be accessed by the class where it is defined.

Let's continue to look at examples:

class Employee{
    private allocatebonus?: number   //allocatebonus is a private property and an optional parameter
    private bonus: number = 0
    constructor(public name:string, public salary:number){
        this.name = name
        this.salary = salary
    }

! [screenshot 7.08.59 PM 2021-11-21] (/ users / Feng / library / Application Support / typora user images / screenshot 7.08.59 PM 2021-11-21. PNG)

At this time, we cannot access allocatebonus and bonus in the instance

3. getter/setter

We can write the function in the form of calling field in the class

class Employee{
    private allocatebonus?: number
    constructor(public name:string, public salary:number){
    }
    set bonus(v: number){
        this.allocatebonus = v
    }
    get bonus(){
        return this.allocatebonus || 0
    }
}

Instantiation:

const em = new Employee('jhon', 9000)
em.bonus = 2000
console.log(em)

Output:

Employee: {
  "name": "jhon",
  "salary": 9000,
  "allocatebonus": 2000
} 
4. Class inheritance (method rewriting of inherited classes)

Class inheritance is similar to interface expansion. We can understand it as inheritance in object-oriented language. The old class is called the parent class, and the new class is called the subclass. The subclass inherits the properties and methods of the parent class through the keyword extends

After class inheritance, subclasses can redefine the methods of the parent class. This process is called method rewriting.

The super keyword is a direct reference to the parent class, which can reference the properties and methods of the parent class.

//Parent class
class Employee{
    private allocatebonus?: number
    constructor(public name:string, public salary:number){
    }
    set bonus(v: number){
        this.allocatebonus = v
    }
    get bonus(){
        return this.allocatebonus || 0
    }

}

//Subclass
class Manager extends Employee{
    private reporters: Employee[]
     constructor(name:string, salary:number) {
        super(name, salary)
        this.reporters = []
     }
     addReporters(e: Employee){
         this.reporters.push(e)
     }
}

const manager = new Manager('DF', 200000)

After class inheritance is successful, we can see the following figure:

! [screenshot 11.29.34 PM, 2021-11-21] (/ users / Feng / library / Application Support / typora user images / screenshot 11.29.34 PM, 2021-11-21. PNG)

3, Implementing interfaces with classes

After learning the contents of the previous two parts, we can now use classes to implement interfaces. Let's take a look at the simple implementation:

1. Interface implicit implementation
//Interface
interface Emploeey{
    name: string
    salary: number
}

//class
class Emplmpl{
    name: string
    salary: number
    constructor( name:string, salary:number){  //Constructor
        this.name = name
        this.salary = salary
    }
}
//Here, we may not be able to directly see how the class implements the interface. In TS, as long as the class meets the attributes and methods of the interface, that is, the class implements the interface
const emplpml = new Emplmpl('ice_moss', 10000)

In this way, the interface is implemented by the class. Of course, it can also be declared here:

const emplpml = new Emplmpl('ice_moss', 10000)
const em1: Emploeey = emplpml

Here's a summary: the above implementation is implicit. When you are asked to assign a value, the compiler will compare the fields in the interface and class one by one; But there is also a problem here. If we lack the attributes or methods in the interface in the class, we may not be able to implement the interface by using the implicitly implemented method, but the compiler will not remind us.

2. Interface display implementation

We can use classes to implement interfaces or display implementations, which can inform us of errors in the implementation process in advance

interface Emploeey{
    name: string
    salary: number
}

class Emplmpl implements Emploeey{  //Display implementation of interface
    name: string
    salary: number
    constructor( name:string, salary:number){  //Constructor
        this.name = name
        this.salary = salary
    }
}

const emplpml = new Emplmpl('ice_moss', 10000)
3. How to select implicit implementation - display implementation

Here's an example: we have a front-end applet. We have many services, such as pages, interfaces, etc

Definer = implementer = > display implementation

Define and implement interfaces in the Servce.ts file

//Servce.ts
//Define interface
interface Servce {
    login(): void         //Sign in
    getTrips(): string    //Get travel
    getLic(): string      //Get driver's license number
    startTrip(): void     //Starting stroke
    updataLic(lic: string): void  //Update driver's license number
}
//Implementation interface
class RPCservce implements Servce {
    login(): void {
        throw new Error("Method not implemented.");
    }
    getTrips(): string {
        throw new Error("Method not implemented.");
    }
    getLic(): string {
        throw new Error("Method not implemented.");
    }
    startTrip(): void {
        throw new Error("Method not implemented.");
    }
    updataLic(lic: string): void {
        throw new Error("Method not implemented.");
    }
}

Here is a login page file:

//login: Page
//file: Login.ts
const page ={
servce: new RPCservce() as Servce,
onLoginButtonCliked(){   //Login click button
    //Use interface
    this.servce.login()
}}

It can be seen that this is a display implementation. We put all the methods in the interface and implement the interface using class RPCservce implements Servce

So this is: definer = implementer = > display implementation

However, in TS, we prefer implicit implementation:

In our display implementation, we only need to call one method on the login page: this.servce.login() to service

However, many methods (services) will appear when we call, but we don't want to see that we need to use implicit

Implicit implementation: user = implementer = > implicit implementation

The definition of the interface is completed by the user. What methods (services) the user needs and then define the required interface. Let's continue to look at the examples:

//Implementation interface
class RPCservce{
    login(): void {
        throw new Error("Method not implemented.");
    }
    getTrips(): string {
        throw new Error("Method not implemented.");
    }
    getLic(): string {
        throw new Error("Method not implemented.");
    }
    startTrip(): void {
        throw new Error("Method not implemented.");
    }
    updataLic(lic: string): void {
        throw new Error("Method not implemented.");
    }
}


interface Servcelogin {
    login(): void         //Sign in
}

//login: Page login page
//file: Login.ts
const loginPage = {
loginServce: new RPCservce() as Servcelogin,
onLoginButtonCliked(){
    //Use interface
    this.loginServce.login()
}}


interface ServceTrips{
    getTrips(): string    //Get travel
    startTrip(): void     //Starting stroke
}

//Trips: Page trip Page
//file: Trips.ts
const tripsPage = {
 tripsServce: new RPCservce() as Servce,
 tripsButtonCliked(){
     this.tripsServce.getTrips()
 }   
}

In this way, we don't have to use the methods in the previous Servce interface. Which page needs to use what method (service) and which page can define the interface. Each interface is very small, and there may be only one or two functions, which is also convenient for interface maintenance.

4, Generics

1. Introduction

In software engineering, we should not only create consistent and well-defined API s, but also consider reusability. Components can support not only current data types, but also future data types, which provides you with very flexible functions when creating large-scale systems.

In languages like C# and Java, generics can be used to create reusable components. A component can support multiple types of data. In this way, users can select data types to use components according to their own needs.

const a: Array<String> = []
a.push('hhhh')
console.log(a)

const b: Array<number> = []
b.push(100)
console.log(b)

//Output:
["arr", "kk"] 
["hhhh"] 
[100] 

//If there are 10000 data types, do we need to write 10000 such functions, which shows that this is unrealistic, so we need generics to simplify our use:
class MyArray<T> {
    data: T[] = []
    add(t: T){
        this.data.push(t)
    }
    print(){
        console.log(this.data)
    }
}

const test = new MyArray<string>()
test.add('arr')
test.add('kk')
test.print()

//Output:
["arr", "kk"] 

Let's create the first example of using generics: the identity function. This function returns any value passed in. You can think of this function as an echo command.

Without generics, this function may be as follows:

function identity(arg: number): number {
    return arg;
}

Alternatively, we use the any type to define the function:

function identity(arg: any): any {
    return arg;
}

In this way, when the data types are different, we need to write many functions with the same functions and deal with different types. Here, we need to use generics. Generics are like a template, which can be used for any data type.

Using any type will cause this function to receive arg parameters of any type, thus losing some information: the type passed in should be the same as the type returned. If we pass in a number, we only know that any type of value can be returned.

Therefore, we need a way to make the type of the return value the same as the type of the incoming parameter. Here, we use the type variable, which is a special variable used only to represent the type rather than the value.

function identity<T>(arg: T): T {
    return arg;
}

We added the type variable t to identity. T helps us capture the type passed in by the user (such as number), and then we can use this type. Then we use t again as the return value type. Now we can know that the parameter type is the same as the return value type. This allows us to track the type of information used in the function.

We call this version of the identity function generic because it can be applied to multiple types. Unlike using any, it does not lose information, as in the first example, it maintains accuracy, passes in a numeric type and returns a numeric type.

After we define a generic function, we can use it in two ways. The first is to pass in all parameters, including type parameters:

let output = identity<string>("myString");  // type of output will be 'string'

Here, we explicitly specify that T is a string type and pass it to the function as a parameter, using < > instead of ().

The second method is more common. Type inference is used - that is, the compiler will automatically help us determine the type of T according to the passed parameters:

let output = identity("myString");  // type of output will be 'string'

Note that we don'T have to use angle brackets (< >) to explicitly pass in types; The compiler can look at the value of myString and set T to its type. Type inference helps us keep our code concise and highly readable. If the compiler cannot automatically infer the type, it can only explicitly pass in the type of T as above. This may occur in some complex cases.

2. Use generic variables

When using generics to create generic functions such as identity, the compiler requires you to use this generic type correctly in the function body. In other words, you must treat these parameters as any or all types.

Take a look at the previous example of identity:

function identity<T>(arg: T): T {
    return arg;
}

If we want to print the length of arg at the same time. We are likely to do this:

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}

If you do so, the compiler will report an error that we used the. length attribute of arg, but there is no place to indicate that arg has this attribute. Remember, these type variables represent any type, so the person using this function may pass in a number without the. length attribute.

Now suppose we want to manipulate an array of type T instead of directly T. Since we operate on arrays, the. length attribute should exist. We can create this array like other arrays:

function loggingIdentity<T>(arg: T[]): T[] {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}

You can understand the type of loggingIdentity in this way: the generic function loggingIdentity receives the type parameter T and the parameter arg. It is an array with element type T and returns an array with element type T. If we pass in an array of numbers, we will return an array of numbers, because the type of T is number. This allows us to use the generic variable t as part of the type rather than the entire type, increasing flexibility.

We can also implement the above example in this way:

function loggingIdentity<T>(arg: Array<T>): Array<T> {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}

If you have used other languages, you may be familiar with this grammar. In the next section, we will describe how to create custom generics like array < T >.

3. Generic type

In the previous section, we created the identity general function, which can be applied to different types. In this section, we look at the types of functions themselves and how to create generic interfaces.

The type of a generic function is no different from that of a non generic function, except that there is a type parameter in the front, like a function declaration:

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: <T>(arg: T) => T = identity;

We can also use different generic parameter names, as long as they correspond in quantity and usage.

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: <U>(arg: U) => U = identity;

We can also define generic functions using object literals with call signatures:

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: {<T>(arg: T): T} = identity;

This leads us to write the first generic interface. We take the object literal in the above example as an interface:

interface GenericIdentityFn {
    <T>(arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn = identity;

For a similar example, we might want to treat a generic parameter as a parameter of the entire interface. In this way, we can clearly know which generic type is used (for example, dictionary < string > instead of just Dictionary). In this way, other members of the interface can also know the type of this parameter.

interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;

Notice that our example has made a few changes. Instead of describing generic functions, non generic function signatures are used as part of generic types. When we use GenericIdentityFn, we have to pass in a type parameter to specify the generic type (here: number) and lock the type used in the code later. For describing which types belong to the generic part, it is helpful to understand when to put parameters in the call signature and when to put parameters on the interface.

In addition to generic interfaces, we can also create generic classes. Note that generic enumerations and generic namespaces cannot be created.

Generic classes look like generic interfaces. Generic classes use (< >) to enclose generic types, followed by class names.

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

The use of the GenericNumber class is very intuitive, and you may have noticed that there is no restriction that it can only use the number type. You can also use strings or other more complex types.

let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };

console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

Just like interfaces, putting generic types directly after classes can help us confirm that all properties of a class are using the same type.

As we said in the class section, a class has two parts: the static part and the instance part. Generic class refers to the type of instance part, so static properties of class cannot use this generic type.

Note: the generic part refers to typescript Official documents

Tags: Front-end TypeScript Vue.js

Posted on Mon, 22 Nov 2021 19:21:31 -0500 by cash09