TypeScript Generics serves as a mechanism to develop reusable components. It enables the creation of components that can operate with multiple data types instead of being restricted to just one. This feature allows users to utilize these components with their own specified types. Generics contributes to the program's adaptability and scalability over time.
Generics offer type safety while ensuring that performance and productivity are not sacrificed. In TypeScript, generics are utilized through the type variable that represents types. The type of generic functions resembles that of non-generic functions, with the type parameters specified first, akin to function declarations.
In generics, it is essential to specify a type parameter within the open (<) and close (>) brackets, which results in strongly typed collections. Generics utilize a specific type variable <T> that represents types. The collections implemented with generics are restricted to containing only homogeneous types of objects.
In TypeScript, it is possible to define generic classes, generic functions, generic methods, and generic interfaces. TypeScript Generics share similarities with generics found in C# and Java.
Example
The following example aids in clarifying the concept of generics.
function identity<T>(arg: T): T {
return arg;
}
let output1 = identity<string>("myString");
let output2 = identity<number>( 100 );
console.log(output1);
console.log(output2);
Upon compiling the aforementioned file, it generates the related JavaScript file as shown below.
function identity(arg) {
return arg;
}
var output1 = identity("myString");
var output2 = identity(100);
console.log(output1);
console.log(output2);
Output:
Advantage of Generics
There are mainly three advantages of generics. They are as follows:
- Type-safety: We can hold only a single type of objects in generics. It doesn't allow to store other objects.
- Typecasting is not required: There is no need to typecast the object.
- Compile-Time Checking: It is checked at compile time so the problem will not occur at runtime.
Why need Generics?
The necessity for generics can be illustrated through the example provided below.
function getItems(items: any[] ) : any[] {
return new Array().concat(items);
}
let myNumArr = getItems([10, 20, 30]);
let myStrArr = getItems(["Hello", "TypeScript Tutorial"]);
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", "TypeScript Tutorial", "Hello SSSIT", 40]
Output:
In the preceding example, the getItems function takes an array parameter that is of type any. The getItems function initializes a new array of type any, appends items to it, and subsequently returns this newly formed array. By utilizing the any data type, we can supply various types of items to the function. However, this approach may not be the most appropriate for adding items. We should ensure that numbers are added exclusively to a number array and strings to a string array, avoiding the addition of numbers to the string array or the other way around.
To address this, TypeScript brought forth generics. Within generics, the type variable is designed to accept only the specific type that the user specifies at the time of declaration. Additionally, it maintains the integrity of type checking information.
Therefore, we can express the aforementioned function as a generic function in the following manner.
function getItems<T>(items : T[] ) : T[] {
return new Array<T>().concat(items);
}
let arrNumber = getItems<number>([10, 20, 30]);
let arrString = getItems<string>(["Hello", "TypeScript Tutorial"]);
arrNumber.push(40); // Correct
arrNumber.push("Hi! TypeScript Tutorial"); // Compilation Error
arrString.push("Hello TypeScript"); // Correct
arrString.push(50); // Compilation Error
console.log(arrNumber);
console.log(arrString);
Output:
In the preceding example, the type variable T denotes the function within the angle brackets getItems<T>. This variable also defines the types of both the arguments and the return value. It guarantees that the data type indicated during a function invocation will be consistent with the data type of the arguments and the return value.
The generic function getItems takes in an array of numbers and an array of strings. When we invoke the function getItems<number>([10, 20, 30]), it will substitute T with the number. Consequently, both the argument types and the return type will be a number array. Likewise, when using the function getItems<string>(["Hello", "TypeScript Tutorial"]), the types of the arguments and the return value will be a string array. If we attempt to insert a string into arrNumber or a number into arrString, the compiler will generate an error. This ensures that the benefits of type checking are maintained.
In TypeScript, it is possible to invoke a generic function without explicitly defining the type variable. The TypeScript compiler will automatically determine the value of T for the function according to the types of the provided argument values.
Multi-type variables
In TypeScript Generics, it is possible to declare variables that can hold multiple types using distinct identifiers. The following example illustrates this concept clearly.
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");
Output:
Generic with non-generic Type
Generic types may also be utilized alongside other types that are not generic.
Example
function displayDataType<T>(id:T, name:string): void {
console.log("DataType of Id: "+typeof(id) + "\nDataType of Name: "+ typeof(name));
}
displayDataType<number>(1, "Abhishek");
Output:
Generics Classes
TypeScript additionally accommodates generic classes. The generic type parameter is indicated within angle brackets (<>) immediately after the class name. A generic class may include generic fields or methods.
Example
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();
Output:
Generics Interface
The interface can likewise utilize the generic type. The following example illustrates how the generic interface can be understood.
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);
Output:
Generics Interface as Function Type
Generics interfaces can also serve as function types. The example below illustrates this concept.
Example
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");
Generic Constraints
TypeScript Generics Types enable us to handle any data type effectively. Nonetheless, we can limit them to specific types through the application of constraints. In the upcoming example, we will define an interface that includes a singular .length property. We will utilize this interface along with the "extends" keyword to specify our constraint.
Example
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
Output:
Length: 10
Length: undefined
Generic Constraints with class
An enhanced illustration of the relationships involving Generic constraints between the constructor function and the instance side of class types is presented below.
Example
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);
Output: