Generics: Generics

A major part of software engineering is building reusable components that have well-declared, stable apis. These components help us build a robust and scalable system. In languages like C# and Java, generics can be used to create reusable components that can support multiple types of data. This allows users to use components with their own data types. The key purpose of generics is to provide meaningful constraints between members: instance members of the class, methods of the class, function parameters, and function return values. Generics is a template that allows the same function to take different types of arguments. It is better to create reusable components using generics than using any type because they preserve parameter types.

The syntax of generics

For those new to TypeScriptThe genericReaders for the first time<T>Grammar will feel strange. There’s nothing special about it, just like passing parameters, we pass the type we want to use for a particular function call.Refer to the picture above when we callidentity<Number>(1)NumberTypes are like parameters1As it will appearTFills any position of the type. In the figure<T>The inside of theTCalled the type variable, it is the type placeholder that we want to pass to the identity function, and it is assigned tovalueArgument is used instead of its type: at this timeTActs as a type, not a specific Number type.

Among themTOn behalf ofTypeIs usually used as the first type variable name when defining generics. But in factTYou can use any valid name instead. In addition toTIn addition, here’s what common generic variables stand for:

  • K (Key) : indicates the Key type of the object.
  • V (Value) : indicates the Value type of an object.
  • E (Element) : indicates the Element type.

Instead of defining only one type variable, you can introduce as many type variables as you define. For example, we introduce a new type variable U to extend our identity function:

function identity<T, U>(value: T, message: U): T {
  console.log(message);
  return value;
}

console.log(identity<Number, string>(68, "Semlinker"));
Copy the code

We can implicitly call a function by passing only arguments, and the compiler will passType parameter reasoning(Type argument inference) to inform the compiler which type we used as a typeT.

function identity <T, U>(value: T, message: U) : T {
  console.log(message);
  return value;
}

console.log(identity(68, "Semlinker"));
Copy the code

This approach seems easier to read and write, but in complex cases, do not omit type variables to avoid unnecessary errors.

A generic interface

interface GenericIdentityFn {
    <T>(arg: T): T;
}
function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: GenericIdentityFn = identity;
Copy the code

Further, we can make the interface a generic interface by extracting the generic type variables into the interface description, so that all functions and attributes in the interface accept the description of the generic type.

interface GenericIdentityFn<T> {
    (arg: T): T;
}
function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
Copy the code

At this point, when we use an interface to describe a variable, we tell the interface what type to accept to describe generics.

A generic class

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) { return x + y; };
Copy the code

When we talked about classes, we said that classes actually have two parts, a static part and an instance part. Generics only cover the instance part. When we use the static part of a class, we cannot use generics to describe its static part.

Generic constraint

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}
loggingIdentity(1)
Copy the code

In this 🌰, we need to access the length attribute of the ARG, and the compiler cannot ensure that all types have this attribute, so it will report an error. Usually when we use this function, we don’t expect all types, but types with the length attribute. So if a variable has some members, and some members are required, we need to make a list of requirements to constrain the incoming types. To solve this problem, we describe our constraints by declaring an interface. Here, we create an interface with the length attribute and have type T extend it as a constraint.

interface Lengthwise {
    length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property, so no more error
    return arg;
}
loggingIdentity(3);  // Error, number doesn't have a .length property
loggingIdentity({length: 10, value: 3});
Copy the code

This means that when we do type checking, we check the parameter to see if the type matches the description of the interface — that is, the object passed in must have an instance part property named Length and of type number. If the parameters of the call do not conform to this constraint, the compiler will report an error. We can also use generics to extend generics.

function copyFields<T extends U, U>(target: T, source: U): T {
    for (let id in source) {
        target[id] = source[id];
    }
    return target;
}

let x = { a: 1, b: 2, c: 3, d: 4 };
copyFields(x, { b: 10, d: 20 }); // okay
copyFields(x, { Q: 90 });  // error: property 'Q' isn't declared in 'x'.
Copy the code

This description means that we accept parameters of two generic types as function parameters, and that the first type is constrained by the second type, that is, the object properties of the second type must exist in the object properties list of the first type. Otherwise, an error will be reported. We can treat the first type as a subclass and the second type as a parent, but the requirements are not as strict as inheritance.

Generic utility classes

For developers’ convenience, TypeScript includes some common tool types, such as Partial, Required, Readonly, Record, and ReturnType. We will only briefly cover Partial tool types here. But before we get into the details, we need to cover some of the basics.

  • keyof

The keyof operator, introduced in TypeScript 2.1, can be used to get all keys of a type whose return type is a union type.

interface Person {
  name: string;
  age: number;
}

type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join" 
type K3 = keyof { [x: string]: Person };  // string | number
Copy the code

There are two index signatures supported in TypeScript, numeric and string:

Interface StringArray {/ / string index - > keyof StringArray = > string | number [index: string] : string; } interface StringArray1 {// index of StringArray1 => number [index: number]: string; }Copy the code

To support both index types, it is required that the numeric index return value be a subclass of the string index return value. The reason for this is that when using numeric indexes, JavaScript converts the numeric index to a string index before performing an index operation. So keyof {[x: string] : Person} returns the result of the string | number.

  • in

In is used to iterate over enumerated types:

type Keys = "a" | "b" | "c"

type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any, c: any }
Copy the code
  • Partial

Partial

makes all the properties of a type optional, okay? . The definition is as follows:

/**
 * node_modules/typescript/lib/lib.es5.d.ts
 * Make all properties in T optional
 */
type Partial<T> = {
  [P in keyof T]?: T[P];
};
Copy the code

In the above code, we first get all the attribute names of T by keyof T, then use in to traverse, assign the values to P, and finally get the corresponding attribute values by T[P]. In the middle? Number to make all properties optional.

interface Todo { title: string; description: string; } function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) { return { ... todo, ... fieldsToUpdate }; } const todo1 = { title: "Learn TS", description: "Learn TypeScript", }; const todo2 = updateTodo(todo1, {description: "Learn TypeScript Enum"});Copy the code

In the updateTodo method above, we defined fieldsToUpdate as Partial

using the Partial

tool type:

{ title? : string | undefined; description? : string | undefined; }Copy the code

The default type of a generic parameter

After TypeScript 2.3, we can specify default types for type parameters in generics. This default type comes into play when type parameters are not specified directly in the code when using generics and cannot be inferred from the actual value parameters.

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