Generics in TypeScript allows us to write a method, function, interface, type alias, or class in such a way that it will allow us to pass in a range of data types. This helps us in re-using the code. In this tutorial let us learn how to use generics in TypeScript using examples.
Table of Contents
Generics in TypeScript
Consider the following functions. Both print whatever is passed to them to the console and then return it to the caller.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | function printANumber(arg: number): number { console.log(arg) return arg; } function printAString(arg: string): string { console.log(arg) return arg; } printANumber(10) //10 printAString("Hello") //Hello |
Both printANumber
and printAString
are similar functions but only differ in the parameter and return data type. The printANumber
function accepts only a number and throws a compiler error if we try to pass any other data type.
The above code is not an efficient way to create functions. The better way is to create a single function in such a way that it can accept any data type but still stays type-safe.
One of the options is to use any as the data type. But that would mean opting out of type checking and defeats the very purpose of using TypeScript.
This is where generics come into the picture. They allow us to create a function, class, or interface using a Generic data type and let us specify the actual data type where we invoke the function or instantiate the class.
Creating the Generic Method
We create a generic method using the syntax
1 2 3 | function print<T>(arg: T): T |
In the above syntax, T is a placeholder for the data type. We enclose it in an angle bracket right after the function name. T is also known as a Type variable, Type parameter, or generic parameter. We substitute T with the actual data type while we invoke the function.
The following is an example of the Generic Method. Here arg
is of type T and also the return type is T.
1 2 3 4 5 6 | function print<T>(arg: T): T { console.log(arg) return arg; } |
We specify the type of T when we invoke the function. For example, we invoke the print function using the syntax print<number>
. Here the type of T is a number, which also implies that the return value is also a number
.
1 2 3 | print<number>(10) //10 |
We can easily pass a string
instead of a number
. Typescript knows that you are passing a string
hence it validates the function assuming the parameter is a string
and also treats the return type as string
.
1 2 3 | print<string>("Hello") |
You can also invoke the function without specifying the data type in the angle bracket. In such cases, TypeScript infers the type (Type inference) from the data type of the Parameter.
1 2 3 4 | print(10) //10 infered type of T is number print("Hello") //Hello infered type of T is string |
In the following example, we try to assign the return value of print(10)
to a string variable. This statement causes an error because the return type is a number because we have passed a number as a parameter to the function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | let n:number let s:string function print<T>(arg: T): T { console.log(arg) return arg; } //Compiler error s = print(10) //Type 'number' is not assignable to type 'string'. n = print("Hello") //Type 'string' is not assignable to type 'number'. //No error n = print(10) s = print("Hello") |
We can pass a value of any data type to a generic function. In this example, we pass a boolean and a Typescript object to the print function.
1 2 3 4 5 6 7 8 9 | function print<T>(arg: T):T { console.log(arg) return arg } print(true) print( { id:"1",name:"Iphone"}) |
We named our generic type variable T. It is just a convention, you can use any valid identifier in its place. The following example uses xy
instead of T.
1 2 3 4 5 6 7 8 9 | function print<xy>(arg: xy):xy { console.log(arg) return arg } print(true) print( { id:"1",name:"Iphone"}) |
The return type need not be a generic type.
1 2 3 4 5 6 7 8 9 | function print<T>(arg: T):boolean { console.log(arg) return true } print(10) print( { id:"1",name:"Iphone"}) |
Even the argument need not be of generic type.
1 2 3 4 5 6 7 8 | function print<T>(arg: number):boolean { console.log(arg) return true } print(10) |
We can also use generic type T to create a local variable. In the following example, the local variable a
is of type T.
1 2 3 4 5 6 7 8 | function print<T>(arg: T):T{ let a:T //a is local variable of type T a=arg console.log(arg) return a } |
You can also use multiple generic types. Each generic parameter is separated by a comma inside the angle bracket.
1 2 3 4 5 6 | function multipleGenericTypes<T,K>(arg1: T, arg2: K) { console.log(arg1) console.log(arg2) } |
Generics Example
The following is one of the real use cases for the generic function. The reverseArray
reverses any array passed on to it. The T[]
indicates the array of type T
. While invoking you can pass an array of strings, numbers, objects, etc.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | function reverseArray<T>(list: T[]) : T[] { const finalList: T[] = []; for (let i = (list.length - 1); i >= 0; i--) { finalList.push(list[i]); } return finalList; } const strArray = ['a', 'b', 'c', 'd']; const reversedStrArray = reverseArray<string>(strArray); console.log(reversedStrArray) // d, c, b, a const numArray = [10, 20, 30, 40]; const reversedNumArray = reverseArray<number>(numArray); console.log(reversedNumArray) // 40, 30, 20, 10 |
Generics Vs Union Types
Union Types can also be used instead of the Typescript Generics. But there are a few differences.
While using union types, we must know all the types upfront. In the following code, we use the union type of number & string. We cannot invoke the print function with any other type other than number or string. For example, if we pass a boolean, it will result in a compiler error. If we use generics then we can pass any data type.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | let n:number let s:string let ns:number|string function print(arg: number|string) : number|string { console.log(arg) return arg; } //all these results in compiler error print(true) //Error because we cannot pass a boolean s = print(10) //Error because s is a string, while return type is number|string s = print("Hello") //Error because s is a string, while return type is number|string n = print("Hello") //Error because n is a number, while return type is number|string n = print(10) //Error because n is a number, while return type is number|string //ok ns is of type number|string ns=print(10) ns = print("Hello") n= print(10) as number // We are converting return type as number |
If the return type changes according to the input types, then we need to return the union type from the function. In the above example, the return type is also a union type of number and string. To capture the return value, we need a create a variable of union type or type cast it
The generic functions can return the generic type.
Generics Vs Function Overloading
Function overloading or method overloading is another way to handle multiple types.
In function overloading, you need to know all the possible types of the parameter and their return types. But unlike union types, the return type need not be type cast, or no need to use a union type.
We can also return different return types than the parameter type. For Example, the Parameter type can be a number, and the return type could be a string, etc. We cannot do that using the generic types.
1 2 3 4 5 6 7 8 9 10 11 12 13 | function print(arg: number):number function print(arg: string):string function print(arg: number| string):number|string { console.log(arg) return arg; } let a = print(10) //Argument is a number & return type is also number let b = print("Hello") let c = print(true) //Error boolean is not accepted. Only number and string is allowed |
When to Use Generic Types
Use generics when you
- Do not know all the possible types
- The return type is a direct mapping