TypeScript y parámetro de tipo no genérico
Considere la siguiente definición de tipo mapeado:
type FeaturesMapEx<T> = {
[K in keyof T]?: T[K];
};
Cuál es un tipo mapeado genérico válido, que usa el tipo genérico T y los campos (claves) de T como un tipo de índice propio. Esos campos contienen el valor del respectivo tipo T[K], lo que básicamente se traduce a
"Aquí tienes un tipo genérico FeaturesMapEx de T. Usa T para obtener sus campos y úsalos como propios. Si T[K] es del tipo X, entonces FeaturesMapEx<T>[K] es del mismo tipo"
Ok. Ahora la cosa que realmente no entiendo, es por qué la declaración no necesita ser como la siguiente, usando dos parámetros genéricos, uno para representar Tipo, el segundo para representar las Claves de ese Tipo:
export type FeaturesMapEx<T, K extends keyof T> = {
[K in keyof T]?: T[K];
};
La declaración anterior es válida, pero K no se utiliza, [K in keyof T] usa un K diferente al definido en la declaración genérica.
O incluso mejor (pero no válida) declaración que tiene más sentido común para mí:
export type FeaturesMapEx<T, K extends keyof T> = {
[K]?: T[K];
};
Lo cual genera un error de TypeScript como el siguiente:
Un nombre de propiedad computada en un literal de tipo debe referirse a una expresión cuyo tipo sea un tipo literal o un tipo 'símbolo único'.ts(1170)
'K' solo se refiere a un tipo, pero se está utilizando como un valor aquí.ts(2693)
Aunque me siento cómodo con el uso de tipos mapeados y genéricos, este ejemplo siempre me marea un poco cuando tengo que escribirlo yo mismo después de un descanso; siempre caigo en patrones presentados más tarde (dos genéricos o error de "tipo usado como valor")... ¿alguien puede explicar si mi confusión es común o si me estoy perdiendo de algo?
Una pregunta adicional es, ¿de dónde proviene el tipo K en la primera declaración, si no es proporcionado por el parámetro de tipo genérico?
Respuesta #1
Considera la interfaz
interface Foo {
a: string,
b: number
}
y el tipo mapeado
export type FeaturesMapEx<T> = {
[K in keyof T]?: T[K];
};
En un tipo mapeado así estamos introduciendo un nuevo parámetro de tipo K (de ahí viene) que está iterando sobre keyof T. Asumiendo que keyof T es un tipo unión, K itera sucesivamente sobre cada componente de esa unión. Se puede pensar en ello como la versión a nivel de tipo del siguiente bucle for en tiempo de ejecución:
function featuresMapEx(t: any) {
for (let k of Object.keys(t)) {
t[k];
}
}
Observe cómo t se introduce en la firma de la función (como la T en la definición del alias de tipo) mientras que k se introduce en un bucle y solo existe en el alcance de ese bucle (como la K se introduce en la clave del tipo mapeado, y solo existe en el alcance de la propiedad del tipo mapeado).
Entonces FeaturesMapEx<Foo> se convierte en {a: string, b: number}.
Si escribiera la siguiente función de JavaScript en su lugar:
function featuresMapEx(t: any, k: any) { // unused parameter here
for (let k of Object.keys(t)) { // oops
t[k];
}
}
puedes ver que estás ignorando completamente el k pasado a la función, al igual que
export type FeaturesMapEx<T, K extends keyof T> = { // unused parameter here
[K in keyof T]?: T[K]; // oops
};
ignora el K pasado a FeaturesMapEx<T, K>.
Entonces este FeaturesMapEx<Foo, 'a'> todavía se convierte en {a: string, b: number}.
En cuanto a tu otra sintaxis:
export type FeaturesMapEx<T, K extends keyof T> = {
[K]?: T[K]; // error
};
Eso ya no sería un tipo mapeado en absoluto, sino una firma de índice, y actualmente no puedes usar tipos de clave arbitrarios en una firma de índice (aunque esto podría suceder después de TS3.5 en algún momento). Un tipo mapeado se parece a una firma de índice pero no lo es. Incluso si eso termina siendo parte del lenguaje, realmente no sería lo mismo... probablemente funcionaría así:
function featuresMapEx(t: any, k: any) {
t[k]; // just one thing
}
Es decir, en realidad no iteraría sobre nada. Puedes obtener este comportamiento ahora más o menos así:
export type FeaturesMapEx<T, K extends keyof T> = Partial<Record<T, K>>
lo cual es lo mismo que el tipo mapeado
export type FeaturesMapEx<T, K extends keyof T> = {
[P in K]?: T[K]
}
pero este FeaturesMapEx<Foo, keyof Foo> se convierte en {a: string | number, b: string | number}. Es decir, las claves serían correctas, pero sus valores serían la unión de los valores que esperas.
De todos modos, espero que la comparación con las funciones en tiempo de ejecución te ayude a que te "haga clic". ¡Buena suerte!
Respuesta #2
...¿de dónde proviene el tipo K en su primera declaración, si no es proporcionado por un parámetro de tipo genérico?
En su primera declaración, el tipo K se introduce en el bucle.
type FeaturesMapEx<T> = {
[K in keyof T]?: T[K]; // this is the loop
};
Aquí hay una analogía suelta que usa una función de JavaScript:
const someObj = {
foo: 'foo-value',
bar: 'bar-value',
baz: 'baz-value',
}
function featuresMapEx(T) {
return Object.keys(T).map(K => `${K}: ${T[K]}`);
}
const result = featuresMapEx(someObj);
console.log(result);