TypeScript Generics is a tool which provides a way to create reusable components. It creates a component that can work with a variety of data types rather than a single data type. It allows users to consume these components and use their own types. Generics ensures that the program is flexible as well as scalable in the long term.
Generics provides type safety without compromising the performance, or productivity. TypeScript uses generics with the type variable which denotes types. The type of generic functions is just like non-generic functions, with the type parameters listed first, similarly to function declarations.
In generics, we need to write a type parameter between the open (<) and close (>) brackets, which makes it strongly typed collections. Generics use a special kind of type variable <T> that denotes types. The generics collections contain only similar types of objects.
In TypeScript, we can create generic classes, generic functions, generic methods, and generic interfaces. TypeScript Generics is almost similar to C# and Java generics.
The below example helps us to understand the generics clearly.
function identity<T>(arg: T): T { return arg; } let output1 = identity<string>("myString"); let output2 = identity<number>( 100 ); console.log(output1); console.log(output2);
When we compile the above file, it returns the corresponding JavaScript file as below.
function identity(arg) { return arg; } var output1 = identity("myString"); var output2 = identity(100); console.log(output1); console.log(output2);
There are mainly three advantages of generics. They are as follows:
We can understand the need for generics by using the following example.
function getItems(items: any[] ) : any[] { return new Array().concat(items); } let myNumArr = getItems([10, 20, 30]); let myStrArr = getItems(["Hello", "rookienerd"]); myNumArr.push(40); // Correct myNumArr.push("Hello TypeScript"); // Correct myStrArr.push("Hello SSSIT"); // Correct myStrArr.push(40); // Correct console.log(myNumArr); // [10, 20, 30, 40, "Hello TypeScript"] console.log(myStrArr); // ["Hello", "rookienerd", "Hello SSSIT", 40]
In the above example, the getItems() function accepts an array which is of type any. The getItems() function creates a new array of type any, concatenates items to it and returns this new array. Since we have used any data type, we can pass any type of items to the function. But, this may not be the correct way to add items. We have to add numbers to number array and the strings to the string array, but we do not want to add numbers to the string array or vice-versa.
To solve this, TypeScript introduced generics. In generics, the type variable only accepts the particular type that the user provides at declaration time. It is also preserving the type checking information.
So, we can write the above function in generic function as below.
function getItems<T>(items : T[] ) : T[] { return new Array<T>().concat(items); } let arrNumber = getItems<number>([10, 20, 30]); let arrString = getItems<string>(["Hello", "rookienerd"]); arrNumber.push(40); // Correct arrNumber.push("Hi! rookienerd"); // Compilation Error arrString.push("Hello TypeScript"); // Correct arrString.push(50); // Compilation Error console.log(arrNumber); console.log(arrString);
In the above example, the type variable T specifies the function in the angle brackets getItems<T>. This variable also specifies the type of the arguments and the return value. It ensures that data type specified at the time of a function call will also be the data type of the arguments and the return value.
The generic function getItems() accepts the numbers array and the strings array. When we call the function getItems<number>([10, 20, 30]), then it will replace T with the number. So, the type of the arguments and the return value will be number array. Similarly, for function getItems<string>(["Hello", "rookienerd"]), the arguments type and the return value will be string array. Now, if we try to add a string in arrNumber or a number in arrString array, the compiler will show an error. Thus, it preserves the type checking advantage.
In TypeScript, we can also call a generic function without specifying the type variable. The TypeScript compiler will set the value of T on the function based on the data type of argument values.
In TypeScript Generics, we can define multi-type variables with a different name. We can understand it with the following example.
function displayDataType<T, U>(id:T, name:U): void { console.log("DataType of Id: "+typeof(id) + "\nDataType of Name: "+ typeof(name)); } displayDataType<number, string>(101, "Abhishek");
We can also use generic types with other non-generic types.
function displayDataType<T>(id:T, name:string): void { console.log("DataType of Id: "+typeof(id) + "\nDataType of Name: "+ typeof(name)); } displayDataType<number>(1, "Abhishek");
TypeScript also supports generic classes. The generic type parameter is specified in angle brackets (<>) following the name of the class. A generic class can have generic fields or methods.
class StudentInfo<T,U> { private Id: T; private Name: U; setValue(id: T, name: U): void { this.Id = id; this.Name = name; } display():void { console.log(`Id = ${this.Id}, Name = ${this.Name}`); } } let st = new StudentInfo<number, string>(); st.setValue(101, "Virat"); st.display(); let std = new StudentInfo<string, string>(); std.setValue("201", "Rohit"); std.display();
The generic type can also be used with the interface. We can understand the generic interface with the following example.
interface People { name: string age: number } interface Celebrity extends People { profession: string } function printName<T extends Celebrity>(theInput: T): void { console.log(`Name: ${theInput.name} \nAge: ${theInput.age} \nProfession: ${theInput.profession}`); } let player: Celebrity = { name: 'Rohit Sharma', age: 30, profession: 'Cricket Player' } printName(player);
We can also use generics interface as function types. The following example can understand it.
interface StudentInfo<T, U> { (id: T, value: U): void; }; function studentData(id: number, value:string):void { console.log('Id = '+ id + ', \nName = ' + value) } let std: StudentInfo<number, string> = studentData; std(11, "Rohit Sharma");
As we know, the TypeScript Generics Types allows working with any and all data type. However, we can restrict it to certain types by using constraints. In the following example, we will create an interface that has a single .length property. We will use this interface, and the "extends" keyword to denote our constraint.
interface Lengthwise { length: number; } function loggingIdentity<T extends Lengthwise>(arg: T): T { console.log("Length: " +arg.length); // It has a .length property, so no more error found return arg; } loggingIdentity({length: 10, value: 9}); loggingIdentity(3); // Compilation Error, number doesn't have a .length property
A more advanced example of Generic constraints relationships between the constructor function and the instance side of class types is given below.
class Student { Id: number; Name: string; constructor(id:number, name:string) { this.Id = id; this.Name = name; } } function display<T extends Student>(per: T): void { console.log(`${ st.Id} ${st.Name}` ); } var st = new Student(101, "\nVirat Kohli"); display(st);