TypeScript - Generics (easy to understand and detailed tutorial)

foreword

Regarding the concept, this article will not describe too much.

Let's look at an example first to experience the problems solved by generics.

We define a printfunction . The function of this function is to print out the parameter passed in, and finally return the parameter. The type of the parameter passed in is string, and the return type of the function is string.

function print(arg: string): string {
    
    
    console.log(arg)
    return arg
}

If the demand has changed now and I still need to print numberthe type , what should I do? Can be retrofitted with union types!

function print(arg:string | number):string | number {
    
    
    console.log(arg)
    return arg
}

Now the requirements have changed again, I still need to print stringarrays , numberarrays, or even any type, what should I do? directly any!

function print(arg:any):any {
    
    
    console.log(arg)
    return arg
}

It should be noted that anythe type of not good, after all, TStry not to write in any.

And this is not the result we want. It can only be said that the incoming value is anyof type and the output value is also anyof type. The incoming and returning are not uniform.

Such writing will even appear bug.

const res: string = print(123) 

Define stringthe type to receive printthe return value of the function, which returns a numbertype , TSand will not prompt us with an error.

At this time, generics appear, which can easily solve the problem of consistent input and output.

In addition, generics are not designed to solve this problem. Generics also solve many other problems. Here is an example to introduce generics.

basic use

The syntax of generics is to write type parameters <>in , which can generally be Trepresented by .

1. Processing function parameters

We use generics to solve the previous problem, as shown in the following code:

function print<T>(arg:T):T {
    
    
    console.log(arg)
    return arg
}

In this way, we have unified the types of input and output, and can input and output any type.

If the types are inconsistent, an error will be reported:

insert image description here

The in the generic Tis like a placeholder, or a variable. When using it, the defined type can be passed in as a parameter , and it can be output intact .

The way of writing generics is a bit weird for front-end engineers, for example <> T, but just remember, as long as you see it <>, you know it is generic.


When we use it, we can specify the type in two ways:

  1. Define the type to use
  2. TS type inference, automatically deduce the type
print<string>('hello')  // 定义 T 为 string
print('hello')  // TS 类型推断,自动推导类型为 string

We know that typeboth interfaceand can define function types, and we can also use generics to write it typelike this:

type Print = <T>(arg: T) => T
const printFn:Print = function print(arg) {
    
    
    console.log(arg)
    return arg
}

interfacewrite this:

interface Iprint<T> {
    
    
    (arg: T): T
}

function print<T>(arg:T) {
    
    
    console.log(arg)
    return arg
}

const myPrint: Iprint<number> = print

2. Default parameters

If you want to add default parameters to generics, you can write like this:

interface Iprint<T = number> {
    
    
    (arg: T): T
}

function print<T>(arg:T) {
    
    
    console.log(arg)
    return arg
}

const myPrint: Iprint = print

In this way, the default is numberthe type . How about it, does it feel Tlike a function parameter?

3. Handling multiple function parameters

Now there is such a function that passes in a tuple with only two items, swaps item 0 and item 1 of the tuple, and returns this tuple.

function swap(tuple) {
    
    
    return [tuple[1], tuple[0]]
}

In this way, we lose the type and use generics to transform it.

We denote the type of item 0 by T and the type of item 1 by U.

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

In this way, the type control of item 0 and item 1 of the tuple can be realized.

insert image description here

Among the parameters passed in, the 0th item is of string type, and the 1st item is of number type.

In the return value of the exchange function, the 0th item is of number type, and the 1st item is of string type.

The 0th item is full of number methods.

insert image description here

Item 1 is full of string methods.

insert image description here

4. Function side effect operation

Generics can not only constrain the parameter types of functions very conveniently, but also can be used when functions perform side-effect operations.

For example, we have a general asynchronous request method and want to return different types of data according to different url requests.

function request(url:string) {
    
    
    return fetch(url).then(res => res.json())
}

Call an interface to obtain user information:

request('user/info').then(res =>{
    
    
    console.log(res)
})

At this time, the returned result res is an any type, which is very annoying.

insert image description here

We hope that calling the API will clearly know what data structure the return type is , so we can do this:

interface UserInfo {
    
    
    name: string
    age: number
}

function request<T>(url:string): Promise<T> {
    
    
    return fetch(url).then(res => res.json())
}

request<UserInfo>('user/info').then(res =>{
    
    
    console.log(res)
})

In this way, the data type returned by the interface can be obtained comfortably, and the development efficiency is greatly improved:

insert image description here

constrained generics

Assuming that there is such a function now that prints the length of the incoming parameter, we write it like this:

function printLength<T>(arg: T): T {
    
    
    console.log(arg.length)
    return arg
}

Because it is not sure whether T has a length attribute, an error will be reported:

insert image description here

So now I want to constrain this generic type, there must be a length attribute, what should I do?

It can be combined with interface to constrain the type.

interface ILength {
    
    
    length: number
}

function printLength<T extends ILength>(arg: T): T {
    
    
    console.log(arg.length)
    return arg
}

The key here is <T extends ILength>to let this generic inherit the interface ILength, so that the generic can be constrained.

The variables we define must have a length attribute, such as str, arr and obj below, so that they can be compiled by TS.

const str = printLength('lin')
const arr = printLength([1,2,3])
const obj = printLength({
    
     length: 10 })

This example also confirms the duck typing of interface again.

As long as you have the length attribute and all meet the constraints, it doesn't matter whether you are str, arr or obj.

Of course, if we define a variable that does not contain a length attribute, such as a number, an error will be reported:

insert image description here

Some applications of generics

Using generics, when defining a function, interface or class, instead of pre-specifying the specific type, you can specify the type when using it.

1. Generic constraint class

To define a stack, there are two methods of pushing and popping. If you want to unify the element types of pushing and popping, you can write like this:

class Stack<T> {
    
    
    private data: T[] = []
    push(item:T) {
    
    
        return this.data.push(item)
    }
    pop():T | undefined {
    
    
        return this.data.pop()
    }
}

Write the type when defining the instance, for example, both stacking and popping must be of type number, just write like this:

const s1 = new Stack<number>()

In this way, an error will be reported when a string is pushed into the stack:

insert image description here

This is very flexible. If the requirements change, both stacking and popping must be of string type. Just change it when defining the instance:

const s1 = new Stack<string>()

In this way, an error will be reported when a number is pushed into the stack:

insert image description here

In particular, generics cannot constrain static members of a class.

Define the static keyword for the pop method, and report an error

insert image description here

2. Generic constraint interface

Using generics, you can also transform the interface to make it more flexible.

interface IKeyValue<T, U> {
    
    
    key: T
    value: U
}

const k1:IKeyValue<number, string> = {
    
     key: 18, value: 'lin'}
const k2:IKeyValue<string, number> = {
    
     key: 'lin', value: 18}

3. Generic definition array

Define an array, we wrote it like this before:

const arr: number[] = [1,2,3]

Now it is also possible to write:

const arr: Array<number> = [1,2,3]

The wrong type of array item is written, and an error is reported

insert image description here

Actual Combat - Generic Constraint Backend Interface Parameter Type

Let's look at a usage that is very helpful for project development and constrains the type of back-end interface parameters.

import axios from 'axios'

interface API {
    
    
    '/book/detail': {
    
    
        id: number,
    },
    '/book/comment': {
    
    
        id: number
        comment: string
    }
    ...
}


function request<T extends keyof API>(url: T, obj: API[T]) {
    
    
    return axios.post(url, obj)
}

request('/book/comment', {
    
    
    id: 1,
    comment: '非常棒!'
})

In this way, there will be a reminder when calling the interface, such as:

The path is wrong:

insert image description here

The parameter type is wrong:

insert image description here

There are fewer parameters:

insert image description here

written in the back

Generics, literally, generics are general and extensive.

Generics means that when defining a function, interface or class, the specific type is not specified in advance, but the type is specified when it is used.


T in generics is like a placeholder, or a variable. When used, the defined type can be passed in as a parameter , and it can be output intact .

Generics provide meaningful constraints between members , which can be: function parameters, function return values, instance members of a class, methods of a class, and so on.


Use a picture to summarize the benefits of generics:

insert image description here

Guess you like

Origin blog.csdn.net/weixin_44198965/article/details/130077755