Signature of call and constructor in Typescript

We already saw a standard introduction to the use of Functions in Typescript. In this post, we will see some of the particularities of typescript that surely no other programming language has and that make TypeScript unique. Let's see the call signatures and constructor signatures.
Call signatures
Call signatures or call signatures is the way of calling a special type in TypeScript that is defined from a function type expression. To understand this a little more, let's observe that all functions defined in the traditional way have the type Function
.
function sum(a: number, b: number): number {
return a + b
}
The Function
type, when describing all functions, is something very similar to the object
type that describes all objects. It is not a good practice to use such a generic type in our code. Observe the following example where a type has been added to the previous function.
type Sum = (a: number, b: number) => number;
let sumFunc: Sum = (a: number, b: number): number => {
return a + b
}
In this way, we can reuse this type throughout the code without being too generic. We can also define these call signatures in a short or complete way. Observe the next example.
// Shorthand call signature
type Log = (message: string, userId?: string) => void
// Full call signature
type Log = {
(message: string, userId?: string): void
}
Note: the only difference is that when defined in the complete way, =>
must be replaced by :
.
Another feature of TypeScript taken from JavaScript is that additional properties can be defined for functions (callables). This is possible when defining the call signature in an object.
type Sum = {
operation: string;
(a: number, b: number): number;
}
// for some reason, we can only use const here
const sumFunc: Sum = (a: number, b: number) => {
return a + b;
}
// we define the property after having defined the call signature
sumFunc.operation = 'Suma'
// sumFunc can be used as a function and as an object to access the defined property
console.log('Operation: ' + sumFunc.operation + ', result: ' + sumFunc(4, 5));
Constructor signatures
Constructor signatures or construct signatures are type definitions that can be called with the new
operator. The syntax is almost exactly the same as the call signatures except for the mentioned operator.
A constructor signature may look like the following:
type nameableConstructor = {
new (name: string): Nameable;
}
However, we cannot create a type that meets this specification directly as with any other type.
// this is NOT the way to use this type
let myVar: nameableConstructor ...
Let's carefully observe the following example. It may not be very clear at first, but don't worry, understanding some concepts of any programming language takes some time. After observing the above code, take a look at the explanation below.
interface Nameable {
name: string
}
class User implements Nameable {
public name: string;
public age: number = 0;
constructor(name: string) {
this.name = name;
}
}
type nameableConstructor = {
new (name: string): Nameable;
}
function buildNameableObject(ctor: nameableConstructor) {
return new ctor('Bob');
}
buildNameableObject(User);
Note: First of all, if you haven't had contact with interfaces, I recommend taking a look at the article Interfaces in TypeScript
We can start by observing that we have an interface definition that exposes the name
property of type string. On the other hand, a class User
has been created that implements this interface and receives a name in its constructor that is added to said property of the interface. Nothing new up to here!
What follows is what is really interesting to understand. On the one hand, we have the definition of the constructor signature.
type nameableConstructor = {
new (name: string): Nameable;
}
This is basically the creation of a type alias that requires a constructor that meets the constructor signature (name: string): Nameable
. That is, basically it asks us a class that has that signature in its constructor. Congratulations, we already have that class which is User
since it accepts the indicated argument and since it implements the mentioned interface we get an object of that type.
Now, how is it used? For this, we need another function that accepts said type (nameableConstructor
) and uses it internally.
function buildNameableObject(ctor: nameableConstructor) {
return new ctor('Bob');
}
In this way, we can use it as a constructor and create an object of any class that meets the defined signature, such as the User
object.
// this statement returns the User object
buildNameableObject(User);