Generics
Hello world
Let’s first write an identity function, which will return the value that it has recieved.
1 | function identity(arg: any): any { |
It’s an absolutely right answer. However, by using any
, we are actually losing the information about what the type was when the function returns. If we passed in a number, the only information we have is that we would recieve a value of type any
.
So, we need a way to capture the type of the argument, that’s why we use type variable
, a special kind of variable that works on types rather than values.
1 | function identity<T>(arg: T): T { |
There are two ways to call the function:
1 | // Explicitly |
Working with generic type variables
One thing to notice: make sure the way we use type variable can be applied to every agreeable types.
1 | // Wrong example |
Generic Types
Let’s explore the type of function with type variables.
First, take a look back at the function it self.
1 | function identity<T>(arg: T): T { |
Then we can write the type of function identity()
.
1 | // v1 |
We can also write the generic type as a call signature of an object literal type.
1 | // v2 |
Further, we can take the object literal and move it to an interface, to make it a generic interface.
1 | // v3 |
Sometimes we may want to move the generic parameter to be a parameter of the whole interface. This lets us see what type we’re generic over.
1 | // v4 |
Understanding when to put the type parameter directly on the call signature and when to put it on the interface itself will be helpful in describing what aspects of a type are generic.
Generic Classes
1 | class GenericClass<T> { |
One thing to notice: generic classes are only generic over their instance side rather than their static side, which means that static members cannot use the class’s type parameter.
Generic Constraints
Back to the above example, sometimes we want to write a generic function that works on a set of types where we have knowledge about what capabilities they have. To do so, we can create an interface that describes the constraint, and then make the type variable extends
the interface to denote our constraint.
1 | interface Lengthwise { |
Using type params in generic constraints
We can declare a type parameter that is constrained by another type parameter. Take a look at the example below:
1 | function getProperty<T, K extends keyof T>(obj: T, key: K) { |
Here we want to get a property from an object and make sure that we’re not accidentally grabbing a property that does not exist on the obj
, so we’ll place a constraint between the two types.
Using class types in generic constraints
When creating factories in TypeScript using generics, it is necessary to refer to class types by their constructor functions. For example,
1 | function create<T>(c: { new (): T }): T { |
A more advanced example uses the prototype property to infer and constrain relationships between the constructor function and the instance side of class types.
1 | class BeeKeeper { |