Welcome to wechat public account: Front Reading Room

introduce

In software engineering, it is important not only to create consistent, well-defined apis, but also to consider reusability. The ability of components to support not only current data types but also future data types gives you a lot of flexibility when building large systems.

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.

Generic Hello World

Let’s create our first example of using generics: the identity function. This function returns any value passed to it. You can think of this function as the echo command.

Without generics, the function might look like this:

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

Alternatively, we use any to define functions:

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

Using any causes the function to accept arG arguments of any type, and some information is lost: the type passed in should be the same as the type returned. If we pass in a number, we just know that any type of value can be returned.

Therefore, we need a way to make the type of the return value the same as the type of the parameter passed in. Here, we use a type variable, which is a special variable that only represents a type, not a value.

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

Once we have defined a generic function, we can use it in two ways. The first is to pass in all the arguments, including the type arguments:

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

The second method is more common. Makes use of type corollaries — that is, the compiler automatically helps us determine the type of T based on the arguments passed in:

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

Type inference helps us keep our code lean and readable. If the compiler cannot automatically infer the type, it can only pass in the type of T explicitly as above, which is possible in some complicated cases.

Use generic variables

When creating a generic function like Identity using a generic type, the compiler requires that you use the generic type correctly in the function body. In other words, you must treat these parameters as any or all types.

Look at the previous identity example:

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

If we want to print out the length of arg at the same time. We would probably do something like this:

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

These type variables represent arbitrary types, so someone using this function might pass in a number that does not have a.length attribute.

Now suppose we want to operate on an array of type T instead of T directly. Since we’re dealing with arrays, the.length property should be there. 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;
}
Copy the code

The generic type

In this section, we’ll look at the types of functions themselves and how to create generic interfaces.

The type of a generic function is no different from that of a non-generic function, except that there is a type parameter first, like a function declaration:

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

let myIdentity: <T>(arg: T) = > T = identity;
Copy the code

We can also use different generic parameter names, as long as they match in number and usage.

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

let myIdentity: <U>(arg: U) = > U = identity;
Copy the code

We can also define generic functions using object literals with call signatures:

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

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

This led us to write our first generic interface. Let’s take the object literal from the above example as an interface:

interface GenericIdentityFn {
    <T>(arg: T): T;
}

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

let myIdentity: GenericIdentityFn = identity;
Copy the code

As a similar example, we might want to treat a generic parameter as a parameter to the entire interface. This way we know exactly which generic type to use (e.g., Dictionary

not just Dictionary). This allows other members of the interface to know the type of the parameter.

interface GenericIdentityFn<T> {
    (arg: T): T;
}

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

let myIdentity: GenericIdentityFn<number> = identity;
Copy the code

Note that our example has changed a little. Instead of describing generic functions, nongeneric function signatures are used as part of generic types. When we use GenericIdentityFn, we also pass in a type parameter to specify the generic type (in this case: number), locking the type to be used later in the code. Understanding when to put parameters in the call signature and when to put parameters on the interface is helpful in describing which part of a type is a generic part.

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

A generic class

Generic classes look much like generic interfaces. Generic classes use (<>) to enclose generic types after the class name.

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

As with interfaces, placing generic types directly after classes helps us ensure that all attributes of a class are using the same type.

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

Generic constraint

You’ll remember from an earlier example where we sometimes wanted to manipulate a set of values of a certain type, and we knew what properties that set of values had. In the loggingIdentity example, we want to access the LENGTH attribute of the ARG, but the compiler can’t prove that every type has the length attribute, so we get an error.

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

Instead of operating on all types of any, we want to restrict the function to any type with a.length attribute. As long as the type passed in has this attribute, we allow, that is, at least include this attribute. To do this, we need to list the constraints on T.

To do this, we define an interface to describe the constraints. Create an interface that contains the.length attribute and use this interface and the extends keyword to implement the 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;
}
Copy the code

Now the generic function is constrained, so it no longer applies to any type:

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

We need to pass in values that match the constraint type and must contain the required attributes:

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

Use type parameters in generic constraints

You can declare one type parameter and it is bound by another type parameter. For example, now we want to get the property from the object using the property name. And we want to make sure that this property exists on the object obj, so we need to use constraints between the two types.

function getProperty(obj: T, key: K) {
    return obj[key];
}

let x = { a: 1.b: 2.c: 3.d: 4 };

getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
Copy the code

Use class types in generics

When TypeScript uses generics to create factory functions, you need to reference the class type of the constructor. For instance,

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

A more advanced example uses stereotype attributes to infer and constrain the relationship of a constructor 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 createInstance<A extends Animal> (c: new () => A) :A {
    return new c();
}

createInstance(Lion).keeper.nametag;  // typechecks!
createInstance(Bee).keeper.hasMask;   // typechecks!
Copy the code

Welcome to wechat public account: Front Reading Room