Do not linger to gather flowers to keep them, though you have walked on, for flowers will keep themselves blooming all your way.

— Tagore “Stray Birds”

In the last article, I introduced TypeScript to type declarations, which are the basics of TypeScript. The content of the presentation includes:

  1. Syntax for specifying a type:: type
  2. Specified as base type:boolean,number,string,null,undefiendsymbol
  3. Specifies the object type
    • Common objects:interface Type {... }
    • Array:type[](type1 | type2 | ...) []
  4. Extension type
    • Literals types: string literals, numeric literals, and Boolean literals
    • Enumeration:enum Colors {... }

Next up: object-oriented programming, access control modifiers, classes and interfaces, generics.

Let’s do it one by one.

Object-oriented programming

Properties and methods

ES6 introduces the class keyword, which brings the “class” -like capability to JavaScript.

Let’s start with some JavaScript code:

class Cat {
  constructor(name) {
    this.name = name
  }

  sayHi() {
    return `Meow, my name is The ${this.name}`}}let tom = new Cat('Tom')
tom.sayHi() // Meow, my name is Tom
Copy the code

We declare a class Cat with an attribute name and a method sayHi. How do you rewrite in TypeScript and add type restrictions? To do this:

class Cat {
  name: string;

  sayHi(): string {
    return `Meow, my name is ${this.name}`}}let tom = new Cat()
tom.name = 'Tom'
Copy the code

In TypeScript classes, when instance properties are introduced or set using a method such as this.name, use a prop: Property ‘name’ does not exist on type ‘Cat’. Also, we restrict the return type of the instance method to a string.

Access control modifier

JavaScript does not have a concept of private properties, and we usually implement “private properties” using conventions or function scopes. TypeScript implements this feature by introducing access control modifiers.

Access-control modifiers are keywords that restrict access to class members (attributes or methods). For example, public is used to modify common properties or methods.

Class modifiers include: private, protected, and public (default).

For each property and method of a class, you can precede it with a modifier that limits its accessibility:

class Cat {
  name: string
    
  constructor(name: string) {
    this.name = name
  }

  sayHi(): string {
    return `Meow, my name is ${this.name}`}}Copy the code

Where nam is a shared property, that is, after returning an instance object with new Cat(‘foo’), we can access the property name as.name, but if we add a private modifier to name, The name property is now private and is no longer accessible on the instance through.name. The keyword protected limits class members to the current class (Cat in this case) and its subclasses.

To sum it up:

The current class A subclass The instance
private ✔ ️
protected ✔ ️ ✔ ️
public ✔ ️ ✔ ️ ✔ ️

Classes and interfaces

Class inheritance

Inheritance can enhance the reusability of code by abstracting the methods and attributes common to subclasses. For some subclasses with inconsistent performance, they can override the attributes or methods of the parent class to define their own logic.

// Define an 'Animal' class here
class Animal {
    name: string;
    
    constructor(name) {
        this.name = name
    }
    
    // contains a method 'sayHi'
    sayHi() {
    	console.log('Hi'); }}// Class 'Cat' inherits from 'Animal'
class Cat extends Animal {
    constructor(name) {
        super(name)
    }
    
    // The 'sayHi' method defined here overrides the one defined in the parent class
    sayHi() {
    	console.log(`Meow, my name this ${this.name}`);
    }
    // In addition to method 'sayHi', subclass 'Cat' defines its own method 'catchMouse'
    catchMouse() {
    	console.log('Caught a mouse')}}Copy the code

Class implementation interface

Interfaces are abstractions of behavior.

As an example, look at the following code:

// Bird has two methods: fly and jump
class Bird {
    fly() {
        console.log('Birds are flying')
    }
    jump() {
        console.log('Birds are jumping')}}// Bees, like birds, have a 'fly' method and the ability to gather 'honey' in addition to their first time.
class Bee {
    fly() {
        console.log('The bees are flying')
    }
    honey() {
        console.log('The bees are gathering honey')}}Copy the code

Bird and Bee have a method with the same name. We can abstract common properties and methods of similar classes into interfaces.

Fly () interface Wings {fly(): void; fly(): void; }Copy the code

Then let Bird and Bee implement this interface:

// Declare the interface to be implemented using the keyword 'implements'
// A class that implements an interface (in this case, 'Bird') must implement all properties or methods defined in the interface
class Bird implements Wings {
    // Because the interface 'Wings' is inherited, the' fly 'method must be implemented
    fly() {
        console.log('Birds are flying')}// 'jump' is a method unique to 'Bird'
    jump() {
        console.log('Birds are jumping')}}// 'Bee' also implements' Wings', so also implements the 'fly' method
class Bee implements Wings {
    fly() {
        console.log('The bees are flying')}// Besides' fly ', bees have also defined their own 'honey' method
    honey() {
        console.log('The bees are gathering honey')}}Copy the code

The function of an interface is to define a unified interface in a unified place, so that the implementation of the interface class has a unified behavior.

So what are the limitations of inheriting classes and implementing interfaces? The answer is: a class can only inherit from one class, but can implement multiple interfaces.

Let’s take an example:

// the 'Wings' interface defines a method' fly '
interface Wings {
  fly(): void;
}
// The interface 'Mouth' defines a method 'sing'
interface Mouth {
  sing(): void;
}
// Declare an abstract class 'Animal'
abstract class Animal {
  // Methods modified with the keyword 'abstract' are called abstract methods.
  // Abstract methods are implemented by inherited classes
  abstract eat(): void;
}
// The 'Bird' class inherits from 'Animal' and implements two interfaces' Wings' and 'Mouth'
class Bird extends Animal implements Wings, Mouth {
  fly() {
      console.log('Birds are flying')
  }
  eat() { ... } // This is a method defined in interface 'Wings' and must be implemented
  sing() { ... } // This is a method defined in interface 'Mouth' and must be implemented
}
Copy the code

As you can see, abstract classes are very similar to interfaces, so what’s the difference? Let’s start with common ground: common methods are defined and then implemented by concrete classes.

The difference is:

  1. Interfaces, like plug-ins, are used to enhance classes, whereas abstract classes are abstractions of concrete classes. Like the birds here (Bird) is the animal (Animal).
  2. Class implementation interface is many-to-many relationship, a class can implement more than one interface, an interface can also be implemented by more than one class; Class inheritance class is a one-to-many relationship, a class can only have one parent class, a class can have multiple subclasses.

Note: In an abstract class, in addition to defining abstract methods, you can also write the implemented methods directly, so that instances of subclasses that inherit the abstract class automatically have those implemented methods.

Interface inheritance class

In addition to extending interfaces, interfaces can also extend classes.

// Declare a class 'Dragon'
class Dragon {
    fly() {
        console.log('Dragons are flying')}}// Interface 'FireDragon' inherits' Dragon '
// This interface has an extra definition for the '() => string' method 'fly'
interface FireDragon extends Dragon {
    fire(): void
}
// We declare the variable 'f' to be of type 'FireGragon', so 'f' must implement both 'fire' and 'fly' methods
// Otherwise an error will be reported
let f: FireGragon = {
    fire() {
        console.log('The dragon is breathing fire')
    }
    
    fly() {
        console.log('Dragons are flying')}}Copy the code

Generic function

Generic function

What if you need to design a function that takes an argument of type number and returns a value of type number? Through previous learning, we can write:

function double(num: number): number {
    return num * 2
}
Copy the code

So what if the return type of a function is always the same as the type of the argument passed in? This introduces the concept of generics.

When we declare functions, we can specify the use of generics.

Let’s write a function:

// We declare a function 'createArray'
function createArray(value, length) {
    let arr = []
    for (let i = 0; i < length; i++) {
        arr[i] = value
    }
    return arr
}

let fooArray = createArray('foo'.3)
fooArray // ["foo", "foo", "foo"]
Copy the code

CreateArray above is a normal function that does not use TypeScript type restrictions. In TypeScript, the default type for undeclared variables is any. So the above code is written the same as:

function createArray(value: any, length: any) :any[] {
    let arr = []
    for (let i = 0; i < length; i++) {
        arr[i] = value
    }
    return arr
}
Copy the code

The inconvenience of this is that when we want to manipulate a member of the array, we can’t write code to give a handy hint because we don’t know the type of the member.

fooArray[0].?????// The editor does not do a good job of giving us code hints because the returned array member type is uncertain
Copy the code

Next we apply generics:

function createArray<T> (value: T, length: number) :T[] {
    let arr = []
    for (let i = 0; i < length; i++) {
        arr[i] = value
    }
    return arr
}

let fooArray = createArray<string> ('foo'.3)
fooArray[0].)// Here, we can get string related property/method hints
Copy the code

Declare the function to receive a generic T in the form of a letter (func

) wrapped in Angle brackets after the function name (in this case, createArray). The parameter and function return value types are also specified as T. In this way, the type of the parameter passed in is the same as the type of the return value of the function.

Next, in the above example, when calling the function, specify that the current generic type T represents a type of string, so that the return value of the function, the type of the array, is determined, that is, an array containing only string members.

It is also possible to call a function without explicitly specifying the specific type represented by the current generic through < XXX >. TypeScript automatically deduces the type of the generic T based on the type of the parameter passed in. I don’t think this is intuitive, so I recommend explicitly specifying generic types when using generics.

It is easy to see that the advantage of using generics is that we can dynamically adjust the specific type T represents. For example, let’s trim it slightly like this:

let fooArray = createArray<number> (100.3)
fooArray[0].)// At this point, we get the code prompt related to the value
Copy the code

Generic scope

Note that the character T used to define functions is not known until after the function is called; And the generic scope is limited only to the scope of the function that declares the generic.

Let arr = [] let arr = [] let arr = []

function createArray<T> (value: T, length: number) :T[] {
    let arr: T[] = []
    for (let i = 0; i < length; i++) {
        arr[i] = value
    }
    return arr
}
Copy the code

We use the generic T not only in function arguments and return values, but also in function scopes.

More generic

Take a look at the following function swap:

// The function 'swap' is used to swap two values in an array
function swap(tuple) {
    return [tuple[1], tuple[0]]}let swapped = swap(['foo'.3])
swapped // [3, "foo"]
Copy the code

The swap function reverses the order of two members in an array. If the two members are of different types, how do we declare the array? As follows:

// Array 'arr' consists of two members, the first
let arr: [string.number] = ['hi'.8]
Copy the code

With generic overrides, instead of using a single generic, you need to use two generics, called multigenerics. We need to specify generics like this:

function swap<T.U> (tuple: [T, U]) :U.T] {
  return [tuple[1], tuple[0]]}Copy the code

A generic interface

Generics can be applied to interfaces as well as functions.

// Here we use an interface 'ListApi', using the generic 'T'
interface ListApi<T> {
    data: T[]; // data is declared as an array of type T
    // This interface also contains a string attribute 'error_message' and a numeric attribute 'status_code'
    error_message: string;
    status_code: number;
}
// Here we specify the generic type 'T' as' {name: string; age: number }`
// 'listResult' becomes an established type
let listResult: ListApi<{ name: string; age: number} >.Copy the code

A generic class

You can also use Angle brackets <> to define generics when defining classes.

Class Component<T> {public props: T; constructor(props: T) { this.props = props; } // Define an interface type interface ButtonProps {color: string; } // Declare 'T' as type 'ButtonProps' when creating an instance of' Component 'let button = new Component<ButtonProps>({
    color: 'red'
});
Copy the code

Component represents a Component class that needs to be passed props of the concrete type represented by the generic T.

If the ButtonProps type is passed in, we need to initialize the ButtonProps parameter according to the structure defined in the ButtonProps.

Contribution refers to the north

Thank you for your precious time reading this article.

If you think this article has made your life a little better, please send me xian (diǎn) hua (zan) or drum (diǎn) Li (zan) 😀. It is always appropriate to leave a comment or opinion below this article, because research shows that participating in a discussion makes people more impressed with knowledge than just reading. Have a nice holiday 😉~.

(after)