Fecha publicación: 25 sept 2021

TypeScript Genéricos

Manos a la obra

Es habitual en TypeScript encontrarnos situaciones donde todavía no sabemos el tipo de datos con el que vamos a trabajar. En dichos casos querremos tipar pero sin tener que recurrir al any, que es potencialmente peligroso. Es decir, quiero establecer un tipado para hacer mi código robusto pero poder especificar más adelante cuando tenga conocimiento exacto del tipo que es. Para todos estos casos se utilizan los Genéricos, es parecido a los que encontramos en otros lenguajes de programación.

Funciones Genéricas

Supongamos el primer ejemplo:

function first(array) {
  return array[0];
}

Esta función sencilla, admite un array como parámetro y devuelve el primer elemento de dicho array. Si quisiera tipar esta función podríamos decir que mi array podría ser de tipo number, en cuyo caso lo que yo devuelvo es un elemento de ese array que tiene que ser un number, esto sería factible y válido.

function first(array: number[]): number {
  return array[0];
}

Que sucedería si mi array es un string:

function first(array: string[]): string {
  return array[0];
}

Pero... ¿ Y si es un boolean, un objeto, una función? No queremos escribir una función específica para cada tipo, nos interesa expresar este tipo como algo genérico que todavía no podemos especificar, pero sin perder el tipado, para hacer nuestra función genérica, nos vamos justo antes de la lista de parámetros y ponemos entre <> el tipo genérico que nosotros deseemos.

Un tipo genérico no es más que un alias, es un nombre que representa un tipo que todavía no sabemos cuál es. Normalmente se utilizan letras en vez de palabras para tipar genéricos por abreviar y hacerlo más corto, por ejemplo la letra T de type.

Nuestra función consta un tipo genérico T y ahora vamos a utilizarlo para tiparlo, nuestros parámetros de entrada serían de tipo T y lo que devuelve también sería de tipo genérico T.

function first<T>(array: T[]): T {
  return array[0];
}

De modo equivalente si nuestra función fuese una arrow function se tiparía de manera igual, primero ponemos nuestro típo genérico T, a continuación la lista de argumentos, nos devuelve un tipo T y con nuestra fat arrow lo implementamos.

const first = <T>(array: T[]): T => array[0];

A la hora de utilizar la función veremos la verdadera ventaja, supongamos un resultado res1 donde vamos a almacenar, y nos devuelve nuestra función first y hacemos la llamada a dicha función. En función de lo que le pasemos como argumento podré especificar el tipo que voy a usar en el genérico (un string, un array...). Si lo que le pasamos es un array de string, le indico que se trata de un string.

const res1 = first<string>(["hello", "world"]);

Si por el contrario lo que estoy pasando es un array de números, en vez de string pondríamos number.

const res2 = first<number>([1, 2, 3]);

No es necesario ponerlo de forma explícita, TypeScript es lo suficientemente inteligente para saber que el array que estamos pasando es de tipo number, por lo tanto mi tipo T es number y no haría falta especificar el genérico.

const res2 = first([1, 2, 3]);

Es más, si le pasamos un array de naturaleza mixta (con diferente tipado), TypeScript es los suficientemente potente para inferir el tipo de res3 . Así pues podemos o bien especificar de forma explicita el tipo que vamos a utilizar o dejar que TypeScript lo haga por nosotros.

Interfaces

Supongamos el siguiente ejemplo:

interface RequestResponse {
    status: number;
    value: any;
}

Con esta interfaz vamos a modelar la respuesta a mi petición, registrando el status en un number y el valor que nos devuelve. Al haber tipado el value con any desconocemos el tipo de valor a devolver cuando realicemos dicha petición.

Este código funciona, pero nos puede llevar a problemas, tendríamos que plantearnos que tipo vamos a usar y sustituirlo por un genérico. Por tanto para hacer una interfaz genérica, al igual que con las funciones, a continuación del nombre añadimos y lo utilizamos en la implementación de nuestro interfaz, es decir, en nuestro value. Le indicamos que vamos a tener un interfaz que es genérico y dicho tipo es el tipo de nuestra propiedad value.

interface RequestResponse<T> {
    status: number;
    value: T;
}

Para utilizar esta interfaz construimos nuestro objeto, donde indicamos el tipo de valor que tiene que tener nuestro value.

const myData: RequestResponse<string> = {
    status: number,
    value: "hola",
}

Si lo cambiásemos por otro tipo, automáticamente nos avisaría TypeScript que debería tener nuestro tipo nuevo.

Lo que no podemos hacer como hacíamos en las funciones es eliminar nuestro genérico y dejar que TypeScript lo interfiera. En las interfaces los tipos genéricos se tienen que especificar en su uso obligatoriamente, lo que si podríamos hacer es establecer un valor por defecto para T en mi tipado.

interface RequestResponse<T = boolean> {
    status: number;
    value: T;
}

const myData: RequestResponse = {
    status: number,
    value: true,
}

Genéricos Múltiples

Por último indicar que los genéricos pueden ser múltiples, podríamos crearnos una función que admita dos argumentos y que lo único que haga es comprobar que el tipo de dichos argumentos es el mismo o no.

const myFunction = (arg1, arg2) => typeof arg1 === typeof arg2;

A la hora de tiparla si quiero hacerlo genérico debe permitir que los dos argumentos sean de cualquier tipo, por lo tanto delante de los parámetros colocamos nuestros <> y necesitamos dos separados por coma <T, U> y tiparnos el retorno que en este caso sería un boolean.

const myFunction = <T, U>(arg1: T, arg2: U): boolean => typeof arg1 === typeof arg2;

En nuestra lista de genérico podemos utilizar tantos como queramos, no hay limitación. Para terminar vamos a probarlo.

console.log(myFunction<string, number>("1", 1));

En este caso nos devolvería false.

generic_function1

Al ser una función, podemos obviar la especificación de tipos y dejar que TypeScript haga la inferencia de tipos de forma automática.

console.log(myFunction("1", 1));

¿Te apuntas a nuestro máster?

Si te ha gustado este ejemplo y tienes ganas de aprender Front End guiado por un grupo de profesionales ¿Por qué no te apuntas a nuestro Máster Front End Online Lemoncode? Tenemos tanto edición de convocatoria con clases en vivo, como edición continua con mentorización, para que puedas ir a tu ritmo y aprender mucho.