1
  • Description : There is currently no Chinese translation of the latest official documents of TypeScript on the Internet, so there is such a translation plan. Because I am also a beginner in TypeScript, I cannot guarantee that the translation will be 100% accurate. If there are errors, please point out in the comment section;
  • translation content : The tentative translation content is TypeScript Handbook , and other parts of the translation document will be added later;
  • project address : TypeScript-Doc-Zh , if it helps you, you can click a star~

The official document address of this chapter: Generics

Generic

A major part of software engineering is building components. Components not only have a well-defined and consistent API, but they are also reusable. If a component can handle both current data and future data, it will provide you with the most flexible capabilities when building large-scale software systems.

In languages such as C# and Java, one of the main tools for creating reusable components is generics. Using generics, we can create components that are applicable to multiple types instead of a single type, allowing users to use their own types when using these components.

Getting to know generics

First acquainted with generics, let us first implement a function identity identity function can accept any parameter and return it. You can think that its function is similar to the echo instruction.

If you do not use generics, then we must specify a specific type identity

function identity(arg: number): number {
    return arg;
}

Or, we can use the any type to describe the identity function:

function identity(arg: any): any {
    return arg;
}

Using any is indeed a common practice, because the function's arg parameter can accept any type or all types of values, but in fact, we lose the information about the return value type of the function. If the parameter we pass is a number, then the only information we can know is that the function can return any type of value.

Instead, we need a way to capture the type of the parameter so that we can also use it to indicate the type of the return value. Here, we will use a type variable, this special variable acts on the type rather than the value.

function identity<Type>(arg: Type): Type {
    return arg;
}

We now give identity function to add a variable of type Type . Type allows us to capture the type of parameters passed in by the user (for example, the type number ), which will be used as information that can be used later. Type in the declaration of the return value type. As you can see from the code, the parameters and return values use the same type, which allows us to receive type information on one side of the function and transmit the information to the other side as the output of the function.

We call this version of the identity function a generic function because it is suitable for a range of types. Different from using any , this function is very clear (for example, it does not lose any type information), and the effect is the same as identity number as the parameter and return value type.

Once the generic identity function is implemented, we can call it in two ways. The first way is to pass all parameters to the function, including type parameters:

let output = identity<string>("myString");
    ^^^^^
        // let output: string

Here, we explicitly set Type to string and use it as the parameter of the function call. Note that the package parameter is <> not () .

The second way is probably the most commonly used. We used type parameter inference here-that is, we want the compiler to automatically set the value of Type

let output = identity("myString");
    ^^^^^
        // let output: string

Note that we do not have to explicitly pass in the type <> The compiler will look at the value myString and use the type of this value as the value of Type Although type parameter inference can effectively ensure the conciseness and readability of the code, in more complex cases, the compiler may not be able to successfully infer the type. At this time, you need to explicitly pass in type parameters as before.

Use generic type variables

After you start using generics, you will notice that every time you create identity , the compiler will force you to think that you have correctly used any generic type parameters in the function body. In other words, it will think that you treat these parameters as arbitrary, all types.

Let's take a look at the identity function we wrote earlier:

function identity<Type>(arg: Type): Type {
    return arg;
}

If we want to print the length of arg to the console every time we call, what should we do? We might try to write the following code:

function loggingIdentity<Type>(arg: Type): Type {
  console.log(arg.length);
                 ^^^^^^
// Property 'length' does not exist on type 'Type'.
  return arg;
}

When we do this, the compiler will throw an error that we are visiting arg of .length members, but had not declared anywhere arg have the members. Remember what we said before, these type variables represent any and all types, so people who use this function may pass in number , which does not have a member of .length

Suppose we actually want this function to process Type type 061a828ad3e5ed instead of directly processing Type . Since the current processing is an array, it must have .length members. We can describe it like we create other types of arrays:

function loggingIdentity<Type>(arg: Type[]): Type[] {
  console.log(arg.length);
  return arg;
}

You can interpret the type of loggingIdentity loggingIdentity accepts a type parameter Type , and a parameter arg , which is an Type type 061a828ad3e6a0. The function finally returns an array of type Type If we pass in number , then the final return is also an array of type number Type will be bound to number . This allows us to use the generic type variable Type as one of the types to be used, rather than as the entire type, thus giving us more flexibility.

We can also rewrite this simple example as follows:

function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
  console.log(arg.length); // 数组有 length 属性,所以不会再抛出错误
  return arg;
}

You may already be familiar with this type of style in other languages. In the next section, we will introduce how to create a generic type Array<Type>

Generic type

In the previous sections, we created a generic function identity suitable for a range of types. In this section, we will explore the type of the function itself and the way to create a generic interface.

Generic function types are the same as non-generic function types, except that the former will first enumerate type parameters like a generic function declaration:

function identity<Type>(arg: Type): Type {
    return arg;
}
let myIdentity: <Type>(arg: Type) => Type = identity;

We can also use different names for the generic type parameters in the type, as long as the number of type variables and the way they are used remain the same:

function identity<Type>(arg: Type): Type {
  return arg;
}
 
let myIdentity: <Input>(arg: Input) => Input = identity;

We can also write a generic type as a call signature of an object literal type:

function identity<Type>(arg: Type): Type {
  return arg;
}
 
let myIdentity: { <Type>(arg: Type): Type } = identity;

Based on this, we can write our first generic interface. We extract the object literal in the above code and put it in an interface:

interface GenericIdentityFn {
    <Type>(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
    return arg;
}
let myIdentity: GenericIdentityFn = identity;

Sometimes, we may want to remove the generic parameter as a parameter of the entire interface. This allows us to see which type of generic type is used (for example, use Dictionary<string> instead of Dictionary ). It also makes the type parameter visible to all other members of the interface.

interface GenericIdentityFn<Type> {
    (arg: Type): Type;
}
 
function identity<Type>(arg: Type): Type {
  return arg;
}
 
let myIdentity: GenericIdentityFn<number> = identity;

Note that our example has become different from before. Now what we have is no longer a generic function, but a non-generic function signature, and it is part of a generic type. Now, when we use GenericIdentityFn , we need to specify the corresponding type parameter (here: number ) to effectively ensure that it is used by the underlying function signature. Understanding when to put type parameters directly into the call signature and when to put type parameters into the interface itself is very useful for describing which parts of a certain type are generic.

In addition to generic interfaces, we can also create generic classes. Note that we cannot create generic enumerations and namespaces.

Generic class

The structure of generic classes and generic interfaces is very similar. The generic class will be followed by <> after the class name, which is a list of generic type parameters.

class GenericNumber<NumType> {
  zeroValue: NumType;
  add: (x: NumType, y: NumType) => NumType;
}
 
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
  return x + y;
};

This method of using the GenericNumber class is very simple, but you may have noticed one thing, that is, we do not restrict the class to only use the number type. Instead, we can use string or even other more complex objects.

let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
  return x + y;
};
 
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

Like interfaces, putting type parameters in the class itself allows us to ensure that all properties of the class are using the same type.

As we mentioned in the chapter class, the class contains two parts: the static part and the instance part. The generic type of a generic class only applies to the instance part and not the static part. Therefore, when using a generic class, static members cannot use the type parameters of the class.

Generic constraints

Remember the previous example of accessing the parameter length? Sometimes, you may need to write a generic function that only acts on certain types, and you have a certain understanding of the characteristics of these types. For example, in the example of loggingIdentity function, we want to access arg of length property, but the compiler can not verify that each type has length property so it warns us that we can not assume that all types have the property.

function loggingIdentity<Type>(arg: Type): Type {
  console.log(arg.length);
                 ^^^^^^
// Property 'length' does not exist on type 'Type'.
  return arg;
}

We don't want the function to handle arbitrary types, but we want to constrain it to only handle arbitrary types length As long as a certain type has this attribute, we are allowed to pass in that type, and conversely, to pass in a certain type, then it must at least have this attribute. In order to achieve this, we must enumerate the requirements Type to restrict the type of Type

To do this, we will create an interface that describes the constraints. Here, we create an length , and then use this interface and the extends keyword to express our constraints:

interface Lengthwise {
  length: number;
}
 
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
  console.log(arg.length); // 现在我们知道它一定是有 length 属性的,所以不会抛出错误
  return arg;
}

Because the generic function is now constrained, it can no longer handle arbitrary, all types:

loggingIdentity(3);
               ^
// Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.                   

Instead, the type of the value we pass in should have all the required attributes:

loggingIdentity({ length: 10, value: 3 });

Use type parameters in generic constraints

You can declare a type parameter and make it constrained by another type parameter. For example, now we need to access the properties of the object through the given property name, then we must ensure that we do not accidentally access properties that do not exist on the object, so we will use a constraint between the two types:

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
    return obj[key];
}
 
let x = { a: 1, b: 2, c: 3, d: 4 };
 
getProperty(x, "a");
getProperty(x, "m");
               ^    
// Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.

Use class types in generics

Under the premise of using TypeScript generics, if the factory pattern is used to create an instance, it is necessary to refer to the class type through the constructor to infer the type of the class. E.g:

function create<Type>(c: { new(): Type }): Type {
  return new c();
}

A more advanced example is the following, using prototype properties to infer and constrain the relationship between the constructor and the instance part of the class type:

class BeeKeeper {
  hasMask: boolean = true;
}
 
class ZooKeeper {
  nametag: string = "Mikle";
}
 
class Animal {
  numLegs: number = 4;
}
 
class Bee extends Animal {
  keeper: BeeKeeper = new BeeKeeper();
}
 
class Lion extends Animal {
  keeper: ZooKeeper = new ZooKeeper();
}
 
function createInstance<A extends Animal>(c: new () => A): A {
  return new c();
}
 
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;

This mode can be used to drive mixed into design mode.


Chor
2k 声望5.9k 粉丝