Concept of Generics
A Generic type is a type that has some relationship to other types and is very flexible as to what other types are. TS has built-in generic types:
Array
The main type is Array, but the items in an Array can be of other types.
1
const names: Array = []; //The type here is actually Array<any>, or it can be written as any[];
Promise
The main type is Promise, but the result of resolve can be other types.
1
2
3
4
5const promise: Promise<string> = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("This is done!");
}, 2000);
});
In a way, Generic types don’t care whether the data is of a specific type, but rather want to store type information outside of the incoming data to get better TypeScript support when using generic types.
Use Generic in functions
In the example, TS only knows that the function return type is object.
1
2
3
4
5function merge(objA: object, objB: object) {
return Object.assign(objA, objB);
}
const mergedObj = merge({name: "Max"}, {age: 30});
mergedObj.age; //An error is reported because TS only knows that an object has been returned, but does not know the properties within the object.Generics can be used in functions to express uncertainty about parameter types or return value types, and angle brackets
<>
can be used to declare generic parameters. In the example, as soon as the object type parameter is passed in when calling the function, T is deduced to be of object type.And
<T, U>
is a generic parameter list, used to define type parameters in functions.<T, U>
in the generic parameter list indicates that there are two generic type parametersT
andU
, which can be used to represent the parameter types of the function (T and U).1
2
3
4
5function merge<T, U>(objA: T, objB: U) { //Put the mouse on merge and you will get T&U (intersection, intersection type).
return Object.assign(objA, objB);
}
const mergedObj = merge({name: "Max"}, {age: 30});
mergedObj.age; //Normal operation, get 30.The return value type of a function is inferred based on the code, TS, in the function body. Generic parameters
<T, U>
are only used to specify the type of parameters and provide support for TS type inference, but have no direct impact on the actual behavior of the function and the inference of the return value type.Note that T and U in the example also include types other than object type.
Type constraint
In the above example, if the parameter is not of object type, incorrect results will be obtained, and the IDE will not report an error. If you want to restrict them to the object type, you should use type restrictions.
Type restrictions use the extends keyword and are only written in parameter lists within
<>
.1
2
3
4function merge<T extends object, U extends object>(objA: T, objB: U) {
return Object.assign(objA, objB);
}
const mergedObj = merge({name: "Max"}, 30); //An error will be reported here, 30 does not conform to the type.Similarly, after type restriction extends, you can also use custom types, union types, etc.
Use generics to ensure that function parameters have certain attributes
In TS, you can use Generic to ensure that a parameter has a certain property. A common method is to use generic constraints, that is, add an
extends
keyword and a constraint type after the generic parameter to limit the type of the generic parameter.1
2
3
4
5
6interface HasName {
name: string;
}
function printName<T extends HasName>(obj: T) { //Force parameters to have name attribute
console.log(obj.name);
}The function in the example receives a parameter of type T, which must satisfy the constraint of
T extends HasName
. If the conditions are not met, the IDE will report an error.Similarly, this method can also be used to limit parameters to a certain method on the prototype object to ensure a certain attribute on its prototype object:
1
2
3
4
5
6interface length {
length: number;
}
function countAndDescribe<T extends Lengthy>(element: T){
if(element.length...){ ... };
}
Use keyof to declare that the parameter is a key in the object
keyof
is used to obtain all attribute names of a type and generate a union type composed of attribute names.keyof
has two usage scenarios:Get the object attribute name and perform type checking
Use
keyof
to get all the property names of an object and form them into a union type. Each member of this union type is a property name of the object. We can use this union type to perform type checking to ensure that we are only accessing properties that the object actually owns. For example:1
2
3function extractAndConvert<T extends object, U extends keyof T>(obj: T, key: U) {
return "Value" + obj[key]; //If U is not declared to be the key of the attribute in T, an error will be reported.
}For example, if the value of parameter obj is
{name = "Derek", age = 18, gender = male}
, then the type of U is'name' | 'age' | 'gender'
.
Define generic type parameters
Using
keyof
can also limit the value range of the parameter when defining a generic type parameter to ensure that it can only take the attribute name of a certain type. For example:1
2
3
4
5
6interface Person {
name: string;
age: number;
}
type PersonKey = keyof Person; // type is 'name' | 'age'Similar to the first scenario.
Generic Class
Using generics to define a class allows certain properties or methods in the class to support multiple types. The syntax for using generics in a class is similar to that in a function. We only need to add
<>
after the class name and specify the generic type parameters in it.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class DataStorage<T> {
private data: T[] = [];
addItem(item: T) {
this.data.push(item);
}
removeItem(item: T) {
this.data.splice(this.data.indexOf(item), 1); //Note that indexOf cannot take effect on reference type data.
}
getItems() {
return [...this.data];
}
}
const container1 = new DataStorage<string>();
container1.addItem("derek");
const container2 = new DataStorage<number>();
const container2 = new DataStorage<object>();
container1.addItem({name: "derek"});
container1.addItem({name: "tom"}});
container1.remove({name: "derek"});
console.log(objStorage.getItems()); //The result here is wrong. Because the removed object is a new object, the address is different from the previous one.When the Generic type can represent both primitive type and reference type parameters, problems will arise in some methods that are not common to the two types.
The solution is to first assign the reference type data (the heap address) to a primitive data (that is, in the stack), and then operate. In this way, you can operate according to the method of primitive type data.
1
2
3
4
5
6const container2 = new DataStorage<object>();
const derekObj = { name: "derek" };
container1.addItem(derekObj);
container1.addItem({name: "tom"}});
container1.remove({derekObj);
console.log(objStorage.getItems()); //Get the correct resultAnother solution is to restrict generics
<T>
so that class can only be applied to primitive types. like:1
class DataStorage<T extends string | number | boolean>{}
You can also introduce new generics such as
<U>
in class methods, if this generic is only needed in a certain method.
The difference between Generic and Union type
If you want to get a function, you can call it with one of these types every time you call it, the union type is suitable; if you want to lock a certain type, Generic is suitable. Generic is used to use the same type throughout the entire class instance created.
1
2
3
4
5
6
7
8
9class DataStorage<string | number | boolean> {
private data: string[] | number[] | boolean[] = [];
addItem(item: string | number | boolean) {
this.data.push(item);
}
removeItem(item: string | number | boolean) {
this.data.splice(this.data.indexOf(item), 1);
}
}The union type is used in the example. In fact, different types of parameters can be added each time
addItem(item)
is called. If defined with generic, the class can only add one type of parameters.
More generic knowledge
https://juejin.cn/post/6844904184894980104 There are generic condition types.