Funciones Genéricas en TypeScript
Pareciera que no hay nada más allá de las funciones y las firmas de llamada y de constructor en Typescript. Sin embargo, aquí es cuando entra el concepto de funciones genéricas. En este post, veremos todo lo relacionado a este tipo de funciones en TypeScript.
Vamos a comenzar este artículo observando el siguiente código:
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);
Ya hemos visto anteriormente el uso de Interfaces en Typescript. Lo realmente interesante de este ejemplo es la función clone
. Esta función toma como parámetro un tipo Contact
y devuelve un clone del mismo tipo.
Si observas con más detenimiento, no hay nada específico un la función de clonado que requiera que se pase un tipo Contact
. Podríamos de hecho, usar clone
para clonar cualquier tipo de objeto, lo único que realmente nos interesa es que el valor de retorno sea del mismo tipo que el parámetro recibido.
Dicho lo anterior, podemos convertir la función clone
en una función genérica así:
function clone<T>(source: T): T {
return Object.apply({}, source);
}
La primera parte después del nombre de la función clone<T>
es el identificador del tipo. Generalmente se utiliza T
pero nada te impide utilizar cualquier otro nombre para el identificador. Una vez definido, puede ser usado en el resto de la firma (source: T): T
. De esta forma, se puede utilizar nuestra función clone con cualquier tipo y siempre comprobará que el tipo del parámetro sea el mismo tipo del retorno.
Veamos ahora un ejemplo más cotidiano. Observa la siguiente función que devuelve el primer elemento de un array.
function firstElement(arr: any[]) {
return arr[0];
}
Esta función toma como parámetro un array de cualquier tipo y devuleve el primer elemento. Hemos utilizado el tipo any
ya que la función puede utilizarse con arrays de cualquier tipo. Sin embargo, esto nos trae un problema, usar any es como no usar tipado. Lo ideal sería, que si tenemos un array de números la función retorne un número, y así con todos los tipos.
Convirtamos esta función en genérica agregando los tipos adecuados.
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
Se ha utilizado un tipo de unión (ver tipos de datos en TypeScript) para agregar el valor undefined
. Con esto, cada vez que use dicha función TypeScript comprobará que el valor de retorno sea del mismo tipo que el de los elementos pasados en el array.
Restricciones
Las restricciones sirven para dar un ámbito un poco menos amplio a los tipos que utilizamos en las signatures. Observa el siguiente ejemplo que obtiene el objeto con el length más alto de dos objetos pasados como parámetro.
function max<T>(a: T, b: T) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
El problema con el anterior código, es que no podemos asegurar que los objetos pasados tengan siempre la propiedad length. Para esto, TypeScript nos permite reducir el scope de estos objetos al agregar una restricción sobre el tipo. Observa el mismo ejemplo con dicha restricción.
function max<T extends { length: number }>(a: T, b: T) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
Así, aunque el tipo puede ser cualquier objeto debe tener al menos la propiedad length. Veamos un ejemplo.
interface Box {
width: number;
length: number;
}
interface Figure {
length: number;
}
const box: Box = { width: 10, length: 20 };
const fig: Figure = { length: 5 };
// retorna el objeto Box
max(box, fig);
Nota que aunque los dos objetos pasados como parámetros tiene una interfaz distinta, ambos tiene la propiedad lenght. Esto funciona ya que typescript tiene un Sistema Estructural de Tipos. Sobra decir también que se puede extender de otros tipos previamente definidos.
interface Lengthable {
length: number;
}
function max<T extends Lengthable>(a: T, b: T) {
...
}