skip to content
usubeni fantasy logo Usubeni Fantasy

Análisis de los genéricos en TypeScript

/ 6 min read

This Post is Available In: CN EN ES JA

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:

function identity<Type>(arg: Type): Type {
return arg;
}
let output = identity<string>("myString");

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:

function identity<T>(arg: T): T {
return arg;
}

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:

let output = identity("myString");
// output también es de tipo string

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.

interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // arg debe tener la propiedad length, pasa la verificación de tipo
return arg;
}

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:

function identity<Type>(arg: Type): Type {
return arg;
}

A continuación, veamos un ejemplo más evidente de “vinculación de tipos”.

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // es posible
getProperty(x, "m"); // error, porque `Key` está vinculado a la clave de `Type`, y 'm' no es una clave de `Type`

Tipos mapeados

const myMap = {
a: () => {},
b: (someString: string) => {},
c: (someNumber: number) => {},
};
type MyMap = typeof myMap;
type MyKey = keyof MyMap;

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?

const myMap = {
a: () => {},
b: (someString: string) => {},
c: (someNumber: number) => {},
};
function wrapper<K extends keyof typeof myMap>(key: K, fn: (typeof myMap)[K]) {
return async function (...arg: Parameters<typeof fn>) {
// do something
await Promise.resolve();
fn.apply(null, arg);
};
}
const wrappedMap: {
[K in keyof typeof myMap]: ReturnType<typeof wrapper>;
} = {} as any;
for (const key in myMap) {
const k = key as keyof typeof myMap;
wrappedMap[k] = wrapper(k, myMap[k]);
}

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.

const myMap = {
a: () => {},
b: (someString: string) => {},
c: (someNumber: number) => {},
};
type MyMap = typeof myMap;
type MyKey = keyof MyMap;
function wrapper<K extends MyKey, T extends MyMap[K]>(_key: K, fn: T) {
return async function (...arg: Parameters<T>) {
await Promise.resolve();
(fn as any).apply(null, arg);
};
}
type WrappedMap = {
[K in MyKey]: ReturnType<typeof wrapper<K, MyMap[K]>>;
};
const wrappedMap: Partial<WrappedMap> = {};
for (const key in myMap) {
const k = key as MyKey;
wrappedMap[k] = wrapper(k, myMap[k]);
}

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

const myMap: MyMap = {
a: () => {},
b: (someString: string) => {},
c: (someNumber: number) => {},
};
interface MyMapArgs {
a: [];
b: [someString: string];
c: [someNumber: number];
}
type MyMap = {
[K in keyof MyMapArgs]: (...args: MyMapArgs[K]) => void;
};
type MyKey = keyof MyMap;
function wrapper<K extends MyKey, F extends MyMap[K]>(_key: K, fn: F) {
return async function (...arg: Parameters<F>) {
await Promise.resolve();
fn.apply(null, arg);
};
}
type WrappedMay = {
[K in MyKey]: ReturnType<typeof wrapper<K, MyMap[K]>>;
};
const wrappedMap: Partial<WrappedMay> = {};
for (const key in myMap) {
const k = key as MyKey;
wrappedMap[k] = wrapper(k, myMap[k]);
}

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

评论组件加载中……