Análisis de los genéricos en TypeScript
/ 6 min read
Introducción a los genéricos
En pocas palabras, los genéricos se pueden entender como pasar un tipo como variable a una definición de tipo, de la misma manera que se pasan los argumentos a una función, por ejemplo:
Al envolver Type
con <>
, se puede pasar el tipo a la función genérica. El efecto de la función anterior es: aceptar un argumento de tipo Type
y devolver un resultado de tipo Type
.
Dado que Type
es un parámetro, su nombre es libre, a menudo usamos T
como nombre de parámetro, por lo que se puede escribir de la siguiente manera:
Inferencia automática
Cuando se utiliza una función genérica, no es necesario especificar explícitamente el tipo T
(y generalmente no lo hacemos), en este caso, TS inferirá automáticamente el tipo T
:
En el ejemplo anterior, si no se especifica explícitamente el tipo <string>
, TS inferirá directamente que el tipo de "myString"
es T
, por lo que la función también devolverá una cadena.
Restricciones
Por defecto, los genéricos pueden ser de cualquier tipo, lo que reduce la legibilidad y, al operar o llamar a métodos en un tipo “genérico”, no pasará la verificación porque es de cualquier tipo. Para resolver este problema, se puede delimitar el tipo genérico utilizando extends
.
En el código anterior, <T extends Lengthwise>
indica que T
debe ser un tipo con la propiedad length
, cualquier tipo que tenga la propiedad length
cumple con el requisito de este genérico, por ejemplo, también puede ser un array.
Capacidad de vinculación
Según la documentación oficial, los genéricos se utilizan para reutilizar tipos, y después de la breve introducción anterior, seguramente se considera muy efectivo. Pero además de la reutilización de tipos, ¿qué otros usos tiene el genérico?
Mi respuesta es la vinculación de tipos, T puede vincularse a otros genéricos utilizados en la misma definición de tipo.
Echemos otro vistazo a este ejemplo, en realidad está vinculando el tipo de entrada con el tipo de salida:
A continuación, veamos un ejemplo más evidente de “vinculación de tipos”.
Tipos mapeados
Supongamos que tenemos un objeto con claves a, b, c, y los valores son diferentes funciones. Ahora necesitamos obtener un tipo de objeto con la clave y los argumentos correspondientes de la función, ¿cómo se puede lograr esto?
Explicación
En este ejercicio, se nos pide completar el tipo de la variable wrappedMap
en base a la función wrapper
y el objeto myMap
.
Primero, definimos myMap
como un objeto que contiene funciones con diferentes parámetros.
Luego, definimos la función wrapper
que toma una clave (key
) y una función (fn
) como argumentos. La función wrapper
devuelve una función asíncrona que realiza alguna operación antes de llamar a la función original. Los argumentos de la función devuelta son los mismos que los de la función original.
Para completar el tipo de wrappedMap
, utilizamos un bucle for...in
para iterar sobre las claves de myMap
. Dentro del bucle, asignamos la clave actual a la variable k
y luego asignamos el resultado de llamar a wrapper
con la clave y la función correspondientes a la propiedad k
de myMap
a la propiedad k
de wrappedMap
.
Para asegurarnos de que el tipo de wrappedMap
sea correcto, utilizamos una anotación de tipo que utiliza un mapeo de claves (keyof
) y valores (ReturnType
) para definir el tipo de cada propiedad de wrappedMap
.
Finalmente, utilizamos as any
para evitar errores de tipo en la asignación de propiedades dentro del bucle for...in
.
Ahora, de hecho, el tipo de WrappedMap
es el resultado de la función wrapper
, pero, ¿no parece un poco extraña la línea (fn as any).apply(null, arg)
?
¿Por qué necesitamos convertir fn
en any
?
Esto se debe a que, para TypeScript, a
, b
y c
no están vinculados a los tipos de parámetros de sus valores, por lo que incluso si usamos T
para restringirlos, no tiene ningún efecto. Esto puede sonar un poco confuso, pero la respuesta 2 a continuación puede ser más clara.
Respuesta 2
La solución para eliminar (fn as any)
es crear otro mapa que mapee las cosas que deseas relacionar, es decir, MyMapArgs
en el ejemplo anterior. Luego, usa este mapa para crear MyMap
, de esta manera TypeScript finalmente comprende que estas dos cosas están relacionadas.
P.D. Para obtener más información, consulta issues#30581 y pull#47109