Generic Functions in TypeScript.

It seems like there is nothing beyond functions and call and constructor signatures in TypeScript. However, this is where the concept of generic functions comes in. In this post, we will see everything related to this type of functions in TypeScript.
Let's start this article by looking at the following code:
interface Contact {
id: number;
name: string;
}
function clone(source: Contact): Contact {
return Object.apply({}, source);
}
const x: Contact = { id: 1, name: 'Bob' };
const y: clone(x);
We have already seen the use of Interfaces in TypeScript before. The really interesting thing about this example is the clone
function. This function takes a Contact
type as a parameter and returns a clone of the same type.
If you look more closely, there is nothing specific in the cloning function that requires a Contact
type to be passed. We could actually use clone
to clone any type of object, the only thing that really matters to us is that the return value is of the same type as the received parameter.
That being said, we can turn the clone
function into a generic function as follows:
function clone<T>(source: T): T {
return Object.apply({}, source);
}
The first part after the function name clone<T>
is the type identifier. It is generally used T
but nothing prevents you from using any other identifier name. Once defined, it can be used in the rest of the signature (source: T): T
. This way, our clone function can be used with any type and will always check that the parameter type is the same type of the return.
Let's now look at a more common example. Consider the following function that returns the first element of an array.
function firstElement(arr: any[]) {
return arr[0];
}
This function takes an array of any type as a parameter and returns the first element. We have used the any
type since the function can be used with arrays of any type. However, this brings us a problem, using any is like not using typing at all. Ideally, if we have an array of numbers the function should return a number, and so on with all types.
Let's turn this function into a generic function by adding the appropriate types.
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
A union type has been used (see data types in TypeScript) to add the undefined
value. With this, every time we use this function, TypeScript will check that the return value is of the same type as the elements passed in the array.
Constraints
Constraints serve to give a little less broad scope to the types we use in the signatures. Consider the following example that gets the object with the highest length of two objects passed as a parameter.
function max<T>(a: T, b: T) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
The problem with the previous code is that we cannot guarantee that the objects passed always have the length property. For this, TypeScript allows us to reduce the scope of these objects by adding a constraint on the type. See the same example with this constraint.
function max<T extends { length: number }>(a: T, b: T) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
Thus, although the type can be any object, it must have at least the length property. Let's see an example.
interface Box {
width: number;
length: number;
}
interface Figure {
length: number;
}
const box: Box = { width: 10, length: 20 };
const fig: Figure = { length: 5 };
// returns Box object
max(box, fig);
Note that although the two objects passed as parameters have a different interface, both have the length property. This works since TypeScript has a Structural Type System. Needless to say, it can also be extended from other previously defined types.
interface Lengthable {
length: number;
}
function max<T extends Lengthable>(a: T, b: T) {
...
}