Generic

  1. 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:

    1. 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[];

    2. Promise

      The main type is Promise, but the result of resolve can be other types.

      1
      2
      3
      4
      5
      const 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.


  2. Use Generic in functions

    In the example, TS only knows that the function return type is object.

    1
    2
    3
    4
    5
    function 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 parameters T and U, which can be used to represent the parameter types of the function (T and U).

    1
    2
    3
    4
    5
    function 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.


  3. 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
    4
    function 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.


  4. 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
    6
    interface 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
    6
    interface length {
    length: number;
    }
    function countAndDescribe<T extends Lengthy>(element: T){
    if(element.length...){ ... };
    }

  5. 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:

    1. 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
      3
      function 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'.


    1. 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
      6
      interface Person {
      name: string;
      age: number;
      }

      type PersonKey = keyof Person; // type is 'name' | 'age'

      Similar to the first scenario.


  6. 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
    21
    class 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
    6
    const 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 result

    Another 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.


  7. 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
    9
    class 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.


  8. More generic knowledge

    https://juejin.cn/post/6844904184894980104 There are generic condition types.


Share