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 print
function . 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 number
the 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 string
arrays , number
arrays, 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 any
the type of not good, after all, TS
try not to write in any
.
And this is not the result we want. It can only be said that the incoming value is any
of type and the output value is also any
of type. The incoming and returning are not uniform.
Such writing will even appear bug
.
const res: string = print(123)
Define string
the type to receive print
the return value of the function, which returns a number
type , TS
and 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 beT
represented 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:
The in the generic T
is 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:
- Define the type to use
- TS type inference, automatically deduce the type
print<string>('hello') // 定义 T 为 string
print('hello') // TS 类型推断,自动推导类型为 string
We know that type
both interface
and can define function types, and we can also use generics to write it type
like this:
type Print = <T>(arg: T) => T
const printFn:Print = function print(arg) {
console.log(arg)
return arg
}
interface
write 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 number
the type . How about it, does it feel T
like 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.
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.
Item 1 is full of string methods.
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.
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:
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:
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:
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:
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:
In particular, generics cannot constrain static members of a class.
Define the static keyword for the pop method, and report an error
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
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:
The parameter type is wrong:
There are fewer parameters:
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: