typescript - Keyof como genérico
Estoy intentando obligar a alguien que quiera actualizar un objeto a pasar la clave a actualizar con un tipo de valor correcto. Como se ve en el playground, TypeScript parece incapaz de entender que si la key tiene un valor determinado, la variable value tiene un tipo correcto. Mientras se llama a la función, la verificación de tipos es correcta y no permite pasar valores inconsistentes con la clave.
type Person = {
name: string;
age: number;
};
function update<T extends keyof Person>(orig: Person, key: T, value: Person[T]): Person {
if (key === "name") {
return {
...orig,
name: value // Error: number not assignable to string
}
} else if (key === "age") {
return {
...orig,
age: value // Error: string not assignable to number
}
} else {
return orig
}
}
let unknownPerson = {
name: "Unknown",
age: 999
};
unknownPerson = update(unknownPerson, "age", 10);
unknownPerson = update(unknownPerson, "age", "15"); // Ok expected number
Respuesta #1
El compilador es técnicamente correcto al advertirte dentro de la implementación de update(). El parámetro de tipo genérico T puede ser "name" o "age", pero también puede ser el tipo unión "name" | "age".
Es posible (aunque poco probable) que alguien llame a update() con un value que no coincida con key, debido a esto. Por ejemplo:
update(unknownPerson, Math.random() < 2 ? "name" : "age", 100); // no error
// function update<"name" | "age">(
// orig: Person, key: "name" | "age", value: string | number): Person
Entonces, el compilador no asume que verificar key tenga alguna implicación sobre el tipo de value. Si key === "name", el compilador puede restringir el tipo de key, pero no restringe el parámetro de tipo T. Y de ahí el error.
Este es un problema conocido en TypeScript, y hay algunas solicitudes de funciones abiertas destinadas a tratarlo. Consulta microsoft/TypeScript#33014 y microsoft/TypeScript#27808 para leer más. Por ahora, sin embargo, así es como es.
Si necesitas escribir funciones genéricas de esta manera, entonces podrías tener que usar aserciones de tipo para suprimir el error.
Pero en tu caso, realmente no necesitas genéricos aquí. En su lugar, puedes refactorizar la firma de la función de modo que los parámetros de función key y value se representen como elementos de una tupla rest cuyo tipo es una unión discriminada. Así:
type KeyValueTuple<T> =
keyof T extends infer K ? K extends keyof T ? [key: K, value: T[K]] : never : never;
function update(orig: Person, ...[key, value]: KeyValueTuple<Person>): Person {
if (key === "name") {
return {
...orig,
name: value
}
} else if (key === "age") {
return {
...orig,
age: value
}
} else {
return orig
}
}
La función de tipo KeyValueTuple<T> toma un tipo T y produce una unión de tuplas cuyo primer elemento es el tipo de clave (generalmente un tipo literal de cadena) y cuyo segundo elemento es el tipo de valor correspondiente. Puedes ver cómo se comporta esto con Person:
type PersonKeyValueTuple = KeyValueTuple<Person>;
// type PersonKeyValueTuple = [key: "name", value: string] | [key: "age", value: number]
Y luego, después del argumento orig, empaquetamos key y value en un argumento rest del tipo ...[key, value]: KeyValueTuple<Person>. Cabe destacar que dentro de la implementación de la función key y value son aparentemente variables distintas, pero el compilador las trata como uniones discriminadas desestructuradas, por lo que verificar key tiene la implicación requerida para el tipo de value.
Y así lo anterior se compila sin errores.
Enlace de Playground al código
Respuesta #2
Primero puedes crear un clon del objeto original y luego actualizar la clave especificada con el valor dado:
function update<T extends keyof Person>(orig: Person, key: T, value: Person[T]): Person {
const result = { ...orig };
result[key] = value;
return result;
}
De esta manera, tampoco tienes que implementar un manejo especial para cada posible clave.
