preface

The official TypeScript documentation has been updated for a long time, but the Chinese documentation I could find was still in older versions. Therefore, some new and revised chapters are translated and sorted out.

This article is adapted from the TypeScript Handbook chapter “Generics”.

This paper does not strictly follow the translation of the original text, and some of the content has also been explained and supplemented.

The body of the

An important part of software engineering is building components that not only need to have well-defined and consistent apis, but also need to be reusable. Good components that are compatible not only with today’s data types, but also with the data types of the future, give you maximum flexibility when building large software systems.

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

Hello World of Generics

Let’s start writing our first generic, an identity function. An identity function is a function that returns whatever is passed in. You can also think of it as similar to the echo command.

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

function identity(arg: number) :number {
  return arg;
}
Copy the code

Or, we use any:

function identity(arg: any) :any {
  return arg;
}
Copy the code

While using any lets us accept any type of ARG argument, it also lets us 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 type of the parameter and then use it to represent the type of the return value. Here we use a type variable, a special variable that applies to a type, not a value.

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

We have now added a Type variable Type to the identity function, which allows us to capture the user-supplied Type so that we can use it later. Here again, we use Type as the Type of the returned value. In the current notation, it is clear that the parameter and the return value are of the same type.

The current version of the identity function is a generic, which can support passing in multiple types. Unlike using any, it does not lose any information, just as accurately as the first identity function that used number as an argument and return value type.

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

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

Here, we wrap the argument with <> instead of () and explicitly set Type to string as an argument to the function call.

The second method is perhaps more common. Here we use the Type argument inference, in which we hope that the compiler can automatically infer and set the value of type based on the parameters we pass in.

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

Note that we did not pass in an explicit <> Type this time. When the compiler sees the value myString, it automatically sets Type to its Type (that is, string).

Type parameter inference is a useful tool to make our code shorter and easier to read. In more complex cases, when the compiler fails to infer the type, you need to pass in an explicit argument, as in the previous example.

Working with Generic Type Variables

When you create a generic function like Identity, you will find that the compiler forces you to use these type parameters correctly inside the function. This means that you have to take these parameters seriously, considering that they can be any or all types (such as union types).

Let’s take the identity function as an example:

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

What if we wanted to print the length of the ARG argument? We might try something like this:

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

If we do this, the compiler will report an error indicating that we are using the.length attribute of arG, but we have not declared that arG has this attribute elsewhere. We also said earlier that these type variables represent any or all types. So it’s entirely possible that the call is called with a number, but number doesn’t have a.length attribute.

Now suppose this function uses an array of Type instead of Type. Since we are using arrays, the.length attribute must exist. We can write as we would any other type of array:

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

You can think of the Type of loggingIdentity this way: the generic function loggingIdentity takes a Type argument and an argument arg, which is an array of Type. This function returns an array of Type Type.

If we pass in an array of all numeric types, we return an array of all numeric types, because Type is passed in as number.

We now use the Type variable Type as part of the Type we use, rather than as an entire Type, which gives us more freedom.

We could also write the example like this, and it would have the same effect:

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

Generic Types

In the previous section, we created a generic identity function that supports passing in different types. In this section, we explore the types of functions themselves and how to create generic interfaces.

A generic function, like any other non-generic function, requires a list of type arguments. This is a bit like a function declaration:

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

Generic types can use different names for their type parameters, as long as they are consistent in number and usage:

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

We can also write the 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;
Copy the code

This should lead us to write our first generic interface. Let’s use the object literal from the previous example and move its code into the interface:

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

Sometimes we want generic parameters as parameters for the entire interface, which makes it clear what parameters are passed in (for example: Dictionary

instead of Dictionary). It is also visible to other members of the interface.

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

Notice that in this case, we only made a few changes. Instead of describing a generic function, a non-generic function is signed as part of the generic type.

Now when we use GenericIdentityFn, we need to specify the type of the argument. (in this case, number), effectively locking the type used by the calling 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 not possible to create generic enumerated types and generic namespaces.

Generic Classes

Generic classes are written like generic interfaces. Enclose the type parameter list with Angle brackets <> after the class name:

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;
};
Copy the code

In this case, there is no limit to the number type. We could also use a string or even more complex type:

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

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

As we mentioned in the Class section, a Class has two parts of its type: a static part and an instance part. Generic classes only apply to instance parts, so when we use classes, note that static members do not take type parameters.

Generic Constraints

In the earlier loggingIdentity example, we wanted to get the.length property of the arg argument, but the compiler could not prove that every type had the.length property, so it would prompt an error:

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

Rather than being compatible with any type, we would prefer to constrain this function to use only types with the.length attribute. As long as the type has this member, we are allowed to use it, but it must have at least this member. To do this, we need to list the necessary conditions in the Type constraint.

To do this, we need to create an interface that describes the constraints. Here, we create an interface with only the.length attribute, and then we implement the constraint using this interface and the extend keyword:

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;
}
Copy the code

Now that the generic function is constrained, it no longer applies to all types:

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

We need to pass in values that match the constraint:

loggingIdentity({ length: 10.value: 3 });
Copy the code

Using Type Parameters in Generic Constraints

You can declare a type parameter that is constrained by other type parameters. For example, if we want to get the value of a given property name of an object, we need to make sure that we don’t get properties that don’t exist on OBj. So we create 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"'.
Copy the code

Using Class Types in Generics

In TypeScript, when creating instances using the factory pattern, it is necessary to infer the type of classes from their constructors, for example:

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

Here is a more complex example that uses stereotype attributes to infer and constrain, constructor and class instance relationships.

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;
Copy the code

The TypeScript series

  1. The Narrowing of TypeScript
  2. TypeScript More on Functions
  3. The TypeScript Object Type

If you are confused about TypeScript or want to know more about TypeScript, feel free to contact me on wechat: “mqyqingfeng”, public id search: “Yui’S JavaScript blog” or “Yayujs”.

If there is any mistake or not precise place, please be sure to give correction, thank you very much. If you like or are inspired by it, welcome star and encourage the author.