Read the Typescript manual to learn about generics.
Now we want to define a function identity that takes an argument of type number and returns an argument of type number. The usual way to do this is to define the function as follows.
function identity(arg: number) :number {
return arg;
}
Copy the code
Of course that’s fine, now we want this function to be more general, like I want arg to be able to take string arguments, how do I do that?
We can define it as follows:
function identity(arg: number | string) :number | string {
return arg;
}
Copy the code
Or a little rougher
function identity(arg: any) :any {
return arg;
}
Copy the code
But using any here misses the point of using typescript. Now we want to define the type of the parameter passed in, and the type of the parameter may be used in the type definition of the parameter returned. Is there a more elegant way to do this? Of course, the answer is generic. Let’s look at how the above functions are defined using generics.
function identity<Type> (arg: Type) :Type {
return arg;
}
Copy the code
We use <> to define a Type variable after the function. This variable allows us to capture the types of the incoming arguments, and we use this variable as the types of the return arguments. This function now has the flexibility to pass in different types of arguments, which is the concept of generics. Unlike any, we don’t lose the type.
So how do you call this method? There are two ways:
Method 1: Specify the types of parameters to be passed
let output = identity<string> ("myString");
Copy the code
Use <> when calling a method to explicitly indicate the type of parameters to be passed.
Method two: Use type inference
let output = identity("myString");
Copy the code
In this case, the compiler will automatically infer for us the type of the parameters we passed in. The benefit is that the code is cleaner and more readable. However, in complex scenarios where the compiler cannot infer the type, we can use method 1 to explicitly specify the type of the input parameter.
Use generic variables
In this example we can pass in any type of argument,
function identity<Type> (arg: Type) :Type {
return arg;
}
Copy the code
But if I want to print the length property of the ARG
function loggingIdentity<Type> (arg: Type) :Type {
console.log(arg.length);
// Property 'length' does not exist on type 'Type'.
return arg;
}
Copy the code
Now ts is going to make an error, because I can actually pass in any type of argument, like number, and number doesn’t have a length attribute. So what do we do? We can say that we need to pass in an array of Type Type, since arrays have a length attribute, and return an array of Type.
function loggingIdentity<Type> (arg: Type[]) :Type[] {
console.log(arg.length);
return arg;
}
Copy the code
Here Type is more flexible as part of the Type we define. We can also use the built-in generic type Array, which we’ll cover later.
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
The generic type
We created a generic function identity, so how do we define the type of this generic function?
function identity<Type> (arg: Type) :Type {
return arg;
}
let myIdentity: <Type>(arg: Type) = > Type = identity;
Copy the code
(arg: Type) =>Type. The Type name is a variable name, which can be arbitrary. For example, it’s ok to do something like this,
function identity<Type> (arg: Type) :Type {
return arg;
}
let myIdentity: <Input>(arg: Input) = > Input = identity;
Copy the code
We can also define generic functions using object literals with call signatures:
function identity<Type> (arg: Type) :Type {
return arg;
}
let myIdentity: { <Type>(arg: Type): Type } = identity;
Copy the code
Further, we can take this object literal and put it in an interface,
interface GenericIdentityFn {
<Type>(arg: Type): Type;
}
function identity<Type> (arg: Type) :Type {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
Copy the code
Not perfect. We want the generic parameter Type to be the parameter of the entire interface, so we know exactly what Type of parameter we’re using, and it’s also visible to all 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
This is a generic type for Interface GenericIdentityFn.
A generic class
The structure of a generic class is similar to that of a generic interface,
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
Like a generic interface, a generic class guarantees that all members of the class use variables of the same type. Note that the class has both a static part and an instance part and only the instance part can use the generic type.
Generic constraint
Going back to the function that we defined earlier,
function loggingIdentity<Type> (arg: Type) :Type {
console.log(arg.length);
// Property 'length' does not exist on type 'Type'.
return arg;
}
Copy the code
We are trying to print the length of arg, but we are not sure whether the parameter passed in has the length property, so ts is wrong. So we need to put some restriction on our generics, requiring that the parameter passed in must have the length property. How do we do that?
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
We defined an Interface Lengthwise and we put our constraint in there and it says that you have to have a length property. Then we use the extends keyword to add our restriction, which now requires that the passed argument must have the length property.
loggingIdentity(3);
// Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
Copy the code
If not, ts will report an error.
On the other hand, if you have the length property, you’re fine.
loggingIdentity({ length: 10.value: 3 });
Copy the code
Use type parameters in generic constraints
Now we want to define a method that gets the value of an object attribute based on its key, but we also want to prevent situations where the key that is accessed does not exist in the object. Here we can use one type parameter as a constraint on another type parameter,
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
Here we restrict the Key to be a Key of Type. If not, ts will report an error.
Use class types in generics
A factory function that passes in a constructor and returns an instance. We can define the type of the constructor by generics.
function create<T> (c: new () => T) :T {
return new c();
}
Copy the code
Expand usage to incorporate generic constraints.
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
Here we restrict that the type parameter A must be A subclass of Animal.
The resources
- Generics