1
头图

Preface

The official documentation of TypeScript has long been updated, but the Chinese documents I can find are still in the older version. Therefore, some new and revised chapters have been translated and sorted out.

This article is organized from the " Generics " chapter in the TypeScript Handbook.

This article does not strictly follow the original translation, but also explains and supplements part of the content.

text

An important part of software engineering is building components. Components not only need to have well-defined and consistent APIs, they also need to be reusable. Good components are not only compatible with today's data types, but also applicable to data types that may appear in the future, which will give you the greatest flexibility when building large-scale software systems.

In languages such as C# and Java, the tools used to create reusable components are called generics. Using generics, we can create a component that supports many types, which allows users to consume (consume) these components using their own types.

A preliminary study of Generics (Hello World of Generics)

Let's start writing the first generic, an identity function. The so-called identity function is a function that returns whatever is passed in. You can also understand it as similar to the echo command.

Without the help of generics, we may need to give the identity function a specific type:

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

Or, we use the any type:

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

Although using the any type allows us to accept any type of arg parameter, it also allows us to lose the type information when the function returns. If we pass in a number, the only information we know is that the function can return any type of value.

So we need a way to capture the parameter type, and then use it to indicate the type of the return value. Here we use a type variable (type variable) , a special variable used for type rather than value.

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

Now we have added a type variable Type to the identity function. This Type allows us to capture the type provided by the user so that we can use this type in the future. Here, we again use Type as the type of the returned value. In the current way of writing, we can clearly know that the type of the parameter and the return value are the same.

Now this version of the identity function is a generic type, which can support multiple types. Different from using any , it does not lose any information, just as accurate as the first identity function that number

After we have written a generic identity function, we can call it in two ways. The first way is to pass in all parameters, including type parameters:

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

Here, we use <> instead of () wrap the parameters, and explicitly set Type to string as a parameter of the function call.

The second way may be more common, here we use type argument inference (type argument inference) (some Chinese documents will be translated as " type argument inference "), we hope the compiler can automatically based on the parameters we pass in Infer and set the value of Type

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

Note that this time we did not use <> specify the incoming type. When the compiler sees myString , it will automatically set Type as its type (ie string ).

Type parameter inference is a very useful tool, it can make our code shorter and easier to read. In some more complex examples, when the compiler fails to infer the type, you need to explicitly pass in the parameters as in the previous example.

Using Generic Type Variables (Working with Generic Type Variables)

When you create identity , you will find that the compiler will force you to use these type parameters correctly in the function body. This means that you must take these parameters seriously, considering that they may be any one, or even all types (for example, using a combined type).

Let us take the identity function as an example:

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

What if we want to print the length of the arg We might try to write:

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

If we do this, the compiler will report an error, arg that we are using the .length attribute of 061d45cbb031cf, but we have not declared that arg has this attribute elsewhere. We also said earlier that these type variables represent any or all types. It is entirely possible, when incoming call is a number type, but number not .length property.

Now assume this function, using Type type of array instead Type . Because we are using an array, the .length attribute must exist. We can write like other types of arrays:

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

You can understand loggingIdentity like this: the generic function loggingIdentity accepts a Type type parameter and an actual parameter arg , the actual parameter arg is an Type type 061d45cbb03223. The function returns an Type type 061d45cbb03224.

If we pass in an array of all numbers, our return value is also an array of all numbers, because Type will be passed in number

Now we use the type variable Type as part of the type we use instead of the entire type before, which will give us more freedom.

We can also write this example like this, the effect is the same:

function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
  console.log(arg.length); // Array has a .length, so no more error
  return arg;
}

Generic Types

In the last chapter, we have created a generic identity function, which can support passing in different types. In this chapter, we explore the types of functions themselves and how to create generic interfaces.

The form of a generic function is the same as that of other non-generic functions. It requires a list of type parameters first, which is a bit like a function declaration:

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

Generic type parameters can use different names, as long as the number and usage are consistent:

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

We can also write this generic type in the form of the call signature of the object type:

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

This can lead us to write the first generic interface, let us use the object literal in the previous example, and then move its code to the interface:

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

Sometimes, we would like to use generic parameters as the parameters of the entire interface, which allows us to clearly know what parameters are passed in (for example: Dictionary<string> instead of Dictionary ). And other members in the interface can also be seen.

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

Note that in this example, we only made a few changes. No longer describe a generic function, but a non-generic function signature as part of the generic type.

Now when we use GenericIdentityFn , we need to specify the type of the parameter. (In this example, it is number ), which effectively locks the type used by the call signature.

When describing a type that contains generics, it is useful to understand when to put the type parameter in the call signature and when to put it in the interface.

In addition to generic interfaces, we can also create generic classes. Note that it is impossible to create generic enumerated types and generic namespaces.

Generic Classes

The writing of generic classes is similar to generic interfaces. After the class name, use <> angle brackets to wrap the type parameter list:

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;
};

In this example, there is no restriction that you can only use the number type. We can also use string or even more complex types:

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

Just like interfaces, putting type parameters on the class ensures that all properties in the class use the same type.

As we mentioned in the Class chapter, a class has two parts: a static part and an instance part. Generic classes are only valid for the instance part, so when we use classes, note that static members cannot use type parameters.

Generic Constraints

In the earlier loggingIdentity example, we want to get the .length arg , but the compiler cannot prove that each type has the .length attribute, so it will prompt an error:

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

Rather than being compatible with any type, we are more willing to constrain this function so that it can only use the type .length As long as the type has this member, we are allowed to use it, but it must have at least this member. To this end, we need to list the necessary conditions Type

To do this, we need to create an interface to describe constraints. Here, we create an .length attributes, and then we use this interface and the extend keywords to implement constraints:

interface Lengthwise {
  length: number;
}
 
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
  console.log(arg.length); // Now we know it has a .length property, so no more error
  return arg;
}

Now this generic function is constrained, it is no longer applicable to all types:

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

We need to pass in values that meet the constraints:

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

Using Type Parameters in Generic Constraints

You can declare a type parameter, and this type parameter is constrained by other type parameters.

For example, we want to get the value of a given attribute name of an object. To do this, we need to make sure that we don't get obj that don't exist on 061d45cbb034b9. So we establish 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"'.

Using Class Types in Generics (Using Class Types in Generics)

In TypeScript, when using the factory pattern to create an instance, it is necessary to infer the type of the class through their constructor, for example:

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

The following is a more complex example, using prototype properties to infer and constrain the relationship between constructors and class instances.

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;

TypeScript series

The TypeScript series of articles consists of three parts: official document translation, important and difficult analysis, and practical skills. It covers entry, advanced, and actual combat. It aims to provide you with a systematic learning TS tutorial. The entire series is expected to be about 40 articles. Click here to browse the full series of articles, and suggest to bookmark the site by the way.

WeChat: "mqyqingfeng", add me to the only reader group in Kongyu.

If there are mistakes or not rigorous, please correct me, thank you very much. If you like or have some inspiration, star is welcome, which is also an encouragement to the author.


冴羽
9.3k 声望6.3k 粉丝

17 年开始写前端文章,至今 6 个系列,上百篇文章,全网千万阅读