Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

TypeScript generics


May 07, 2021 TypeScript


Table of contents


TypeScript Generics Introduction

In software engineering, we should not only create a well-defined API, but also consider reusability. Components can support not only current data types, but also future data types, providing you with flexible features when creating large systems.

In languages such as C# and Java, generics can 泛型 to create reusable components that can support multiple types of data. This allows users to use components in their own data types.

Hello World for generics

Let's create the first example of using generics: the identity function. T his function returns any value that is passed in to it. You can think of this function as echo command.

Without generics, this function might look like this:

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

Alternatively, we use any type to define functions:

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

Although this function is already able to receive any type of arrg parameters after using any type, it loses some information that the incoming type should be the same as the returned type. If we pass in a number, we only know that any type of value is likely to be returned.

Therefore, we need a way to make the type of return value the same as the type of the incoming argument. Here, we use the type variable, which is a special variable that is used only to represent the type, not the value.

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

We added the type variable T to the identity. T us capture the type that the user is passing in (for example: number and then we can use that type. T hen we use T the return value type. N ow we know that the argument type is the same as the return value type. This allows us to track information about the types used in functions.

We call this version identity function generic because it can be applied to multiple types. Unlike using any it does not lose information, like the first example, which is like maintaining accuracy, passing in a numeric type, and returning a numeric type.

Once we've defined generic functions, we can use them in two ways. The first is to pass in all the parameters, including the type parameters:

let output = identity<string>("myString");  // type of output will be 'string'

Here we explicitly specify T string type and pass it to the function as an argument, using the <> ()

The second method is more common. Using the type inference -- that is, the compiler automatically helps us determine the type of T based on the incoming parameters:

let output = identity("myString");  // type of output will be 'string'

Note that we don't have to use angle brackets to explicitly pass in a type; the myString and then T to its type. <> T ype inference helps us keep our code lean and highly readable. If the compiler can't automatically infer the type, it can only pass in the type of T as explicitly as above, which can occur in some complex cases.

Use generic variables

When you use generics to create generic functions like identity the compiler requires that you use this generic type correctly in the function body. In other words, you have to treat these parameters as arbitrary or all types.

Take a look at the identity example:

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

If we want to print out the length arg the same time. We are likely to do this:

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

If we do this, the compiler will report that we used the .length property of arg but there is no place to indicate arg has this property. Keep in mind that these type variables represent any type, so the person using this function may pass in a number that does not .length attribute.

Now suppose we want to T array of types of T instead of just T B ecause we are operating an array, the .length should exist. We can create this array just like any other array:

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

You can understand loggingIdentity this way: loggingIdentity which receives T and arg which is an array of element types that are T and returns an T I f we pass in an array of numbers, an array of numbers is returned because at this point the type T is number . This allows us to use generic variable T as part of the type, rather than the entire type, adding flexibility.

We can also do the following example:

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

If you have used other languages, you may already be familiar with this syntax. In the next section, you'll show you how to create custom generics like Array<T>

Generic interface

Let's go to the generic interface; let's create a generic identities()

interface Identities<V, W> {
   id1: V,
   id2: W
}
  • 1
  • 2
  • 3
  • 4

I use V and W as type variables here to indicate that any letter (or a valid combination of letter and number names) is available -- their names make no sense except for general purposes.

Now we can apply this interface as the identities() and modify the return type slightly to cater to it. We can console.log and their types to illustrate further:

function identities<T, U> (arg1: T, arg2: U): Identities<T, U> {
   console.log(arg1 + ": " + typeof (arg1));
   console.log(arg2 + ": " + typeof (arg2));
   let identities: Identities<T, U> = {
    id1: arg1,
    id2: arg2
  };
  return identities;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

What identities() passing types T and U to Identities allowing us to define the return types associated with parameter types.

Note: If you compile a TS project and look for generics, no generics are found. B ecause generics are not supported in Javascript, they are not seen in compiler-generated builds. Generics are purely a development safety net for compile time, which ensures that the type of code is securely abstracted.

Generic class

Generic classes look similar to generic interfaces. Generic classes are <> enclose generic types, followed by class names.

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

GenericNumber class is very intuitive, and you may have noticed that there is nothing to limit it to using number type. You can also use strings or other more complex types.

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

alert(stringNumeric.add(stringNumeric.zeroValue, "test"));

As with interfaces, putting generic types directly behind classes can help us confirm that all properties of the class are using the same type.

As we said in the section of the class, the class has two parts: the static part and the instance part. A generic class is a type of the instance part, so the static properties of the class cannot use this generic type.

Generic constraints

You should remember the previous example where we sometimes wanted to manipulate a set of values of a certain type, and we know what properties this set of values has. In loggingIdentity we wanted to access the length property of arg but the compiler didn't prove that each type length property, so it was wrong. length

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

Instead of operating any the types, we want to restrict the function from handling any type with .length property. A s long as the incoming type has this property, we allow it, that is to say, at least it is included. To do this, we need to list the constraint requirements for T.

To do this, we define an interface to describe constraints. Create an interface that .length property, which is used and the extends also implements constraints:

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

Now that this generic function is defined as a constraint, it no longer applies to any type:

loggingIdentity(3);  // Error, number doesn't have a .length property

We need to pass in values that meet the constraint type and must include the required properties:

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

Use type parameters in generic constraints

You can declare a type parameter, and it is constrained by another type argument. Like what

function find<T, U extends Findable<T>>(n: T, s: U) {
  // ...
}
find (giraffe, myAnimals);

Use class types in generics

When TypeScript uses generics to create factory functions, you need to refer to the class type of the constructor. Like what

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

A more advanced example is to use prototype properties to infer and constrain the constructor's relationship to a class instance.

class BeeKeeper {
    hasMask: boolean;
}

class ZooKeeper {
    nametag: string;
}

class Animal {
    numLegs: number;
}

class Bee extends Animal {
    keeper: BeeKeeper;
}

class Lion extends Animal {
    keeper: ZooKeeper;
}

function findKeeper<A extends Animal, K> (a: {new(): A;
    prototype: {keeper: K}}): K {

    return a.prototype.keeper;
}

findKeeper(Lion).nametag;  // typechecks!