Generics
In this post we will have a look at generics in TypeScript and do some examples in code.
But first, what is generics? And what are they good for? Well, short answer is a way of creating reusable components
that are allowing different types
of objects being used when consuming them.
Example - Generic type variables
In our first example, we will create a simple function, which takes in an object of type MyType
and transform it into an array of the same type.
const type: MyType = { name: "John Doe" }; function convertObjectToArray(object: MyType): MyType[] { return [object]; } const myType: MyType = { name: "John Doe" }; const myTypes: MyType[] = convertObjectToArray(myType);
Cool. Let's create a new type, and try to reuse the same function.
type MyOtherType = { age: number }; const myOtherType: MyOtherType = { age: 45 }; const myTypes2: MyOtherType[] = convertObjectToArray(myOtherType); // Error: Type 'MyType[]' is not assignable to type 'MyOtherType[]'.
All right, so that didn't work.. So, let's fix this using Generics
function convertObjectToArray<Type>(object: Type): Type[] { return [object]; } const myTypes: MyType[] = convertObjectToArray<MyType>(myType); const myTypes2: MyOtherType[] = convertObjectToArray<MyOtherType>(myOtherType);
There we go. Now our function will accept all objects of any type.
Example - Generic interface
Let us proceed to next part. Now we will look at how we can use generics in interfaces. We start of with two classes, unrelated from each other.
class Car { hp: number; engineType: "ELECTRIC" | "GAS" | "DIESEL"; constructor(hp: number, engineType: "ELECTRIC" | "GAS" | "DIESEL") { this.hp = hp; this.engineType = engineType; } } class MotorCycle { cc: number; constructor(cc: number) { this.cc = cc; } } const myCar = new Car(240, "GAS"); const myBike = new MotorCycle(150);
Now, we create an interface, that could work fine for both classes.
interface IParkingSpot<T> { vehicle: T; expires: Date; }
And two use this in an example, we can do like this.
const parkingSpotForCar: IParkingSpot<Car> = { vehicle: myCar, expires: new Date("2023-12-31"), }; const parkingSpotForMotorCycle: IParkingSpot<MotorCycle> = { vehicle: myBike, expires: new Date("2023-02-31"), };
Example - Generic constraints
In our first section, we had our function for creating an array out of an object. That particular function, would work for any type of object. In this example, we want to create some more restrictions.
As an example, imagine you have created this super great algorithm or function that works wonders, and you realize that you can reuse this functionality with another set of types in your project. Let create our original function.
interface IOriginalObject { x: number; y: number; someOtherValue: string; } function myBadassFunction(object: IOriginalObject) { return object.x * object.y; }
Now, you have another object of a different type, but that has some similarities to the IOriginalObject
interface
interface INewObject { x: number; y: number; someRandomNumber: number; }
Then, we can modify our function a bit, to accept them both.
interface IAxis { x: number; y: number; } function myBadassFunction<T extends IAxis>(object: T) { return object.x * object.y; }
Now our function will accept all objects, that are of a type
that contains a x
and an y
value.
Example - Class types as generics
In our last example, we will demonstrate how we can use a factory method to create an object with generics.
class ExampleA { num: number | undefined; } class ExampleB { value: number | undefined; } class ExampleC { someProperty: Object; constructor(someProperty: Object) { this.someProperty = someProperty; } }
Given our three classes, we have two without any mandatory parameters, and one which requires an object in the constructor.
Let's create a factory method.
function factory<T>(c: { new (): T }): T { return new c(); } const exampleA: ExampleA = factory(ExampleA); const exampleB: ExampleB = factory(ExampleB);
This method will create an object of the given class we provide it with. But how about classes with mandatory fields? Let's create a new factory.
function factoryWithArgs<T, Arguments>( c: { new (args: Arguments): T }, args: Arguments ): T { return new c(args); } const exampleC: ExampleC = factoryWithArgs(ExampleC, { someProperty: {} });
There we have it. Now we can provide our factory with any class which has required fields in the constructor as well!
Outro
In this guide we showed a couple of ways of how we can use generics in TypeScript. I hope you found it interesting and helpful. To read more about generics, TS docs has some great examples and in depth explanations as well, link to their docs. Have a great day!