In this article, we will go into depth about Generics (Generic data types) in Typescript. For you C # or Java devs, Generic data type is extremely familiar, but for you javascript devs when you first switch to Typescpirt, it will be a bit difficult to approach this concept.
1. Introduction
First, let’s find out what a Generic type is?
As per TypeScript’s definition of Generic:
In languages like C # and Java, one of the main tools in the toolbox for creating reusable components is generics, that is, being able to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.
Simply put, a Generic type is the permission to pass the type to components (function, class, interface) as a parameter. This will make the components more flexible. Better reuse.
2. Why need Generic
We create a function that takes 2 parameters of the same data type (string | number) and returns a Turple .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // union type : `string` and `number` type NS = string | number; // function that returns a tuple function getTuple( a: NS, b: NS ): [ NS, NS ] { return [ a, b ]; } let stringArray = getTuple( 'hello', 'world' ); let numberArray = getTuple( 1.25, 2.56 ); //case error let mixedArray = getTuple( 1.25, 'world' ); // Property 'toUpperCase' does not exist on type 'NS'. console.log( stringArray.map( s => s.toUpperCase() ) ); // Error: Property 'toFixed' does not exist on type 'NS'. console.log( numberArray.map( n => n.toFixed() ) ); |
In the above example the function getTuple
has 2 parameters a
and b
type NS ( Union type ) and returns a tuple [NS, NS]
.
Now we have a few problems with the above function:
- First, we cannot bind
a
andb
have the same data type becausea
andb
can be either string or number. - Second is when fuction returns a tuple (array) containing values of type string or number and Typescript compiler does not allow us to do so because it needs to know the exact data type of the return values. .
The way to solve the problem is to use any type
for a
and b
and tuple [any, any]. Or we can use type assertion to cast a value in tuple ( NS
to string
or number
).
However, both methods can cause errors if we do not manually check the data types of the values.
And Generic type appears, helping us to solve the above problems
Typescript has strong support for generics, we can use generic for function, class, interface …
Now we will modify the above example using a Generic function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // function that returns a tuple function getTuple<T>( a: T, b: T ): [ T, T ] { return [ a, b ]; } let stringArray = getTuple<string>( 'hello', 'world' ); let numberArray = getTuple<number>( 1.25, 2.56 ); let ucStrings = stringArray.map( s => s.toUpperCase() ); let numInts = numberArray.map( n => n.toFixed() ); // Error: Argument of type '"world"' is not assignable to parameter of type 'number'. let mixedArray = getTuple( 1.25, 'world' ); |
We can bind a and b to the same data type using generic type.
When we use getTuple<number>
syntax, the getTuple<number>
compiler will replace T
to number
. So the TS compiler will interpret the getTuple
function as below:
1 2 3 4 | function getTuple( a: number, b: number ): [ number, number ] { return [ a, b ]; } |
Thus the return value of the function is tuple [number, number]
and the TS compiler will let us manipulate this tuple. (Similar to string
)
We can replace T
with any parameter. Syntax f<Type>()
.
In the above example, we noticed, when we called getTuple( 1.25, 'world' )
In the function call, we removed the value of the generic parameter. So TypeScript will infer the data type for T since the type of the first argument1.25 is number
. Since the second argument must be of the same type, this call will result in a compiler error because both arguments must be of the same type as each function declaration. Similarly if we change the first argument to ‘word’ then the TS compiler will deduce the data type of T
is string
.
We can rewrite the function getTuple with the generic arrow function as follows:
1 2 | let getTuple = <T>( a: T, b: T ): [ T, T ] => { ... } |
3. Declare Generic type
We can use generics for function, class, interface …
3.1 Generic Function
As we all know, TypeScript can deduce data types from value. If you hover your mouse over the getTuple function name in your IDE, you’ll see the below return data type of the function. This is the type of function we just created.
1 2 | let getTuple: <T>(a: T, b: T) => [T, T] |
A generic type can contain many different parameters representing many different data types, for example if the two parameters a
and b
are different then we need 2 different parameters to represent their own data type. .
1 2 3 4 5 6 7 8 9 10 11 | let getTuple: <T, U>( a: T, b: U ) => [ T, U ] = ( a, b ) => { return [ a, b ]; } let stringArray = getTuple<string, string>( 'hello', 'world' ); let numberArray = getTuple( 1.25, 2.56 ); let mixedArray = getTuple( 1.25, 'world' ); |
So when we call getTuple( 1.25, 'world' )
the compiler will stop error because the arguments a
and b
can have separate types.
The way to declare the getTuple function is similar:
1 2 3 | type TupleFunc = <T, U>( a: T, b: U ) => [ T, U ]; let getTuple: TupleFunc = ...; |
3.2 Generic Interface
An Interface can also represent a function.
1 2 3 4 | interface MyFunction { (a: number, b: string): any; } |
The above interface will represent a function that takes 2 arguments, number
and string
, and returns a value of type any
We can use this interface to declare the type of the function:
1 2 | let myFunction: MyFunction = ( a, b ) => a+b; |
Arguments a
, b
will accept the type number
and string
. The return value will be of type any
.
3.2.1 Declare a generic function to use interface
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // declare a generic function using interface interface TupleFunction { <T, U>( a: T, b: U ): [ T, U ]; } // declare a function of type `TupleFunction` var getTuple: TupleFunction = ( a, b ) => { return [ a, b ]; } // var stringArray: [string, string] var stringArray = getTuple<string, string>( 'hello', 'world' ); // var numberArray: [number, number] var numberArray = getTuple( 1.25, 2.56 ); // var mixedArray: [number, string] var mixedArray = getTuple( 1.25, 'world' ); |
This example is similar to the previous example, except we create an interface with a generic function. Will give us more flexibility with data types of a
and b
.
3.2.2 Define a generic interface
Interfaces allow us to define the properties and methods of an object. Imagine an object with properties a
and b
and getTuple
function getTuple
returns tuple
with data types of a
and b
?
1 2 3 4 5 6 | interface TupleObject { a: T; // ← invalid b: U; // ← invalid getTuple<T, U>(): [ T, U ]; } |
You may think the above, but this will not be correct. ERROR : cannot find name 'T'
. The correct syntax would be:
1 2 3 4 5 6 | interface TupleObject<T, U> { a: T; b: U; getTuple(): [ T, U ]; } |
Ex:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | let tupleObj1: TupleObject<number, number> = { a: 1, b: 2, getTuple: function() { return [ this.a, this.b ]; } }; // tuple1: [number, number] let tuple1 = tupleObj1.getTuple(); console.log( 'tuple1 =>', tuple1 ); // "tuple1 =>", [1, 2] // var tupleObj2: TupleObject<string, number> let tupleObj2: TupleObject<string, number> = { a: '1', b: 3, getTuple: function() { return [ this.a, this.b ]; } }; // tuple2: [string, number] let tuple2 = tupleObj2.getTuple(); console.log( 'tuple2 =>', tuple2 ); // "tuple2 =>", ["1", 3] |
Consider another scenario for the example above. What if the getTuple function accepts a generic argument?
1 2 3 4 5 6 | interface TupleObject<T, U, V> { a: T; b: U; getTuple( c: V ): [ T, U, V ]; } |
Now the type of c
is fixed according to V
In some cases we want the type of c
be more flexible without depending on the generic TupleObject
we can do:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // declare a generic interface with a genetic method interface TupleObject<T, U> { a: T; b: U; getTuple<V>( c: V ): [ T, U, V ]; } // var tupleObj: TupleObject<string, number> var tupleObj: TupleObject<string, number> = { a: '1', b: 2, getTuple( c ) { return [ this.a, this.b, c ]; } }; // var tuple: [string, number, boolean] var tuple = tupleObj.getTuple<boolean>( true ); console.log( 'tuple =>', tuple ); //"tuple =>", ["1", 2, true] |
3.3 Generic class
Ex:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // declare a generic class class Items<U> { public items: U[]; constructor( ...values: U[] ) { this.items = values; } } // generic class extends another generic class class Collection<T> extends Items<T> { getFirstItem(): T { return this.items[ 0 ]; } } // a collection of `string` items var letters = new Collection<string>( 1, 'b', 'c' ); //=>Argument of type 'number' is not assignable to parameter of type 'string' // var item: string var item = letters.getFirstItem(); console.log( 'item =>', item.toUpperCase() ); |
In the above example, Collection is a generic class, when creating an instance of the Collection class with the keyword new
we provide type T
with syntax new Collection<Type>
. The TS compiler will replace T
with the type we provided.
Type T
is used (inferred) in public
, private
, protected
properties, in methods as well as contructor
. However, for static
property or method, TS compiler does not use generic T
You can convert the generic type from subclass to superclass in inheritance shown below. I have used the U parameter only to prove that the parameter name does not matter between classes and the same applies to Interface.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // declare a generic class class Items<U> { public items: U[]; constructor( ...values: U[] ) { this.items = values; } } // generic class extends another generic class class Collection<T> extends Items<T> { getFirstItem(): T { return this.items[ 0 ]; } } // a collection of `string` items var letters = new Collection<string>( 1, 'b', 'c' ); // var item: string var item = letters.getFirstItem(); console.log( 'item =>', item.toUpperCase() ); //"item =>", "A" |
4. Constraint generic type
4.1 Constraint by extends
Ex:
1 2 3 4 5 6 7 | function merge<U, V>(obj1: U, obj2: V) { return { ...obj1, ...obj2 }; } |
The merge function is a function that merges 2 objects, Ex:
1 2 3 4 5 6 7 | let person = merge( { name: 'John' }, { age: 25 } ); console.log(result); //{ name: 'John', age: 25 } |
It works fine, the merge () function wants you to pass 2 objects, but it doesn’t prevent you from passing it like this:
1 2 3 4 5 6 7 | let person = merge( { name: 'John' }, 25 ); console.log(person); //{ name: 'John' } |
TS doesn’t get any errors, merge () works with all data, but we can force merge function to work with objects. To do that, we use the extends
keyword:
1 2 3 4 5 6 7 | function merge<U extends object, V extends object>(obj1: U, obj2: V) { return { ...obj1, ...obj2 }; } |
Now the merge () function has a data type constraint.
1 2 3 4 5 6 | let person = merge( { name: 'John' }, 25 ); // ERROR : Argument of type '25' is not assignable to parameter of type 'object'. |
4.2 Constraint by extends keyof
Ex:
1 2 3 4 | function prop<T, K>(obj: T, key: K) { return obj[key]; } |
The props function takes 2 parameters, an object, and a key
to the object. The compiler will get the following error:
1 2 | Type 'K' cannot be used to index type 'T'. |
To overcome this, you must bind data type K to be the key
of data type T
1 2 3 4 | function prop<T, K extends keyof T>(obj: T, key: K) { return obj[key]; } |
If you pass prop () a valid key
then the compiler will not issue:
1 2 3 | let str = prop({ name: 'John' }, 'name'); console.log(str);//John |
1 2 3 | let str = prop({ name: 'John' }, 'age'); //error: Argument of type '"age"' is not assignable to parameter of type '"name"'. |
Summary
- Using generic types in typescript to create flexible, reusable funtions, interfaces, classes …
- Use the
extends
keyword to limit a parameter’s data type to a specific data type. - Use the
extends of
keyword to bind the data type to another object’s property.
References
https://www.typescriptlang.org/docs/handbook/generics.html
https://www.tutorialsteacher.com/typescript/typescript-generic
https://medium.com/jspoint/typescript-generics-10e99078cc8