Firma de llamada y de constructor en Typescript
Ya vimos una introducción estándar al uso de Funciones en Typescript. En este post, veremos algunas de las particularidades de typescript que seguramente no tiene ningún otro lenguaje de programación y que hacen de TypeScript único. Veamos pues las firmas de llamada y las firmas de constructor.
Firmas de llamada
Las firmas de llamada o call signatures es la forma de llamar a un tipo especial en typescript que es definido a partir de una expresión de tipo de función. Para entender un poco más esto, observemos que la todas las funciones definidas de la forma tradicional tienen el tipo Function
.
function sum(a: number, b: number): number {
return a + b
}
El tipo Function
, al describir todas las funciones es algo muy similar al tipo object
que describe todos los objetos. No es una buena práctica utilizar un tipo tan genérico en nuestro código. Observa el siguiente ejemplo en donde se ha agregado un tipo a la función anterior.
type Sum = (a: number, b: number) => number;
let sumFunc: Sum = (a: number, b: number): number => {
return a + b
}
De esta forma, podemos reutilizar este tipo a través del código sin ser demasiado genérico. También podemos definir estas firmas de llamada de una forma corta o completa. Observa el siguiente ejemplo.
// Shorthand call signature
type Log = (message: string, userId?: string) => void
// Full call signature
type Log = {
(message: string, userId?: string): void
}
Nota: la única diferencia es que cuando se define de la forma completa debe cambiarse =>
por :
.
Otra característica de TypeScript tomada de JavaScript es que se pueden definir propiedades adicionales a las funciones (callables). Esto esto es posible al definir el call signature en un objeto.
type Sum = {
operation: string;
(a: number, b: number): number;
}
// por alguna razón solo podemos utilizar const aquí
const sumFunc: Sum = (a: number, b: number) => {
return a + b;
}
// definimos la propiedad después de haber definido el call signature
sumFunc.operation = 'Suma'
// sumFunc puede ser usado como función y como objeto para acceder a la propiedad definida
console.log('Operation: ' + sumFunc.operation + ', result: ' + sumFunc(4, 5));
Firmas de constructor
Las firmas de constructor o construct signatures son definiciones de tipo que pueden ser invocadas con el operador new
. La sintaxis es casi exactamente igual a las firmas de llamada con la diferencia que se agrega el operador mencionado.
Una firma de constructor puede lucir de la siguiente manera:
type nameableConstructor = {
new (name: string): Nameable;
}
Sin embargo, no podemos crear directamente un tipo que cumpla esta especificación como ocurre con cualquier otro tipo.
// esta NO es la forma de utilizar este tipo
let myVar: nameableConstructor ...
Observa detenidamente el siguiente ejemplo. Puede parecer no muy claro al comienzo, pero no te preocupes, entender algunos conceptos de cualquier lenguaje de programación toman algún tiempo. Después de observar el código anterior, da un vistazo a la explicación del mismo.
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);
Nota: Antes que nada, si no has tenido contacto con las interfaces te recomiendo darle un vistazo al artículo Interfaces en Typescript
Podemos empezar observando que tenemos una definición de interfaz la cual expone la propiedad name
de tipo string. Por otro lado, se ha creado una clase User
que implementa dicha interfaz y recibe en su constructor un nombre que se añade a dicha propiedad de la interfaz. Nada nuevo hasta aquí!.
Lo que sigue, es lo que realmente interesa entender. Por un lado, tenemos la definición de la firma de constructor.
type nameableConstructor = {
new (name: string): Nameable;
}
Esta es básicamente la creación de un alias de tipo que requiere un constructor que cumpla con la firma del constructor (name: string): Nameable
. Es decir, básicamente nos pide una clase que tenga esa firma en su constructor. En horabuena, ya tenemos dicha clase la cual es User
ya que acepta un el argumento indicado y ya que implementa la interfaz mencionada se obtiene un objeto de dicho tipo.
Ahora bien, y ¿cómo se usa?. Para esto, necesitamos otra función que acepte dicho tipo (nameableConstructor
) y lo use internamente.
function buildNameableObject(ctor: nameableConstructor) {
return new ctor('Bob');
}
De esta manera, podemos utilizarlo como un constructor y crear un objeto de cualquier clase que cumpla con la firma definida, como por ejemplo el objeto User
.
// esta sentencia crear devuelve el objeto User
buildNameableObject(User);