¿Cómo expresar en TypeScript el tipo de un Record restando algunas de sus claves de manera genérica?
Tengo una función que filtra algunas claves de un Record y quiero que la seguridad de tipos me impida acceder a las claves filtradas.
Lo que tengo que expresar es:
// Filters out the result of OnlySelected to remove never from filtered out keys
type OmitNever<T> = { [K in keyof T as T[K] extends never ? never : K]: T[K] }
// Only keeps the keys that are in the type V
type OnlySelected<T extends object, V> = {
[K in keyof T]-?: K extends V ? T[K] : never
}
Lo cual funciona muy bien para casos de uso simples:
const a: A = {
a: 1,
b: "2",
c: "3",
d: "4",
e: "5",
}
type MyType = OmitNever<OnlySelected<A, "a" | "b">>;
// type MyType = {
// a: number;
// b: string;
//}
Pero ahora, si intento usar estos tipos en una función genérica, tengo que tener un medio para convertir el tipo de las claves que quiero conservar a un tipo de unión. Así que tengo que proporcionar el tipo manualmente y es triste tener que repetir las claves a conservar dos veces solo para ser seguro con los tipos:
const filterRecord = <T extends Record<any, any>, TO_KEEP>(record: T, keys: Array<keyof T>) => {
return Object.keys(record)
.reduce((acc, it) => {
if (keys.includes(it)) {
acc[it as keyof OmitNever<OnlySelected<T, TO_KEEP>>] = record[it];
}
return acc;
}, {} as OmitNever<OnlySelected<T, TO_KEEP>>)
}
const res = filterRecord<A, "d" | "e">(a, ["d", "e"]);
console.log(res.d)
console.log(res.e)
console.log(res.a)
¿Alguien sabe la solución o un diseño mejor?
Ver el parque infantil aquí.
Respuesta #1
Puedes definir filterRecord's TO_KEEP como extends keyof T, luego usar TO_KEEP[] como el tipo de parámetro. TypeScript inferirá correctamente entonces (pero sigue leyendo):
const filterRecord = <T extends Record<any, any>, TO_KEEP extends keyof T>(
record: T,
keys: TO_KEEP[]
) => {
return Object.keys(record).reduce((acc, it) => {
if ((keys as readonly string[]).includes(it)) {
acc[it as keyof OmitNever<OnlySelected<T, TO_KEEP>>] = record[it];
}
return acc;
}, {} as OmitNever<OnlySelected<T, TO_KEEP>>);
};
Ejemplo de patio de juegos
Tenga en cuenta que tuve que agregar una aserción de tipo de ampliación dentro de la implementación allí (keys as readonly string[]) para poder usar includes, pero eso es inofensivo.
Nota al margen: A menos que lo estés usando para algún otro propósito, puedes evitar la necesidad de OmitNever modificando ligeramente tu definición de OnlySelected (pero sigue leyendo):
type OnlySelected<T extends object, V> = {
[K in keyof T as K extends V ? K : never]-?: T[K];
// −−−−−−−−−−^^^^^^^^^^^^^^^^^^^^^^^^^^^−−−−−^^^^
};
Ejemplo de patio de juegos
Pero, OnlySelected parece que hace lo mismo que los incorporados Required y Pick, así que podrías simplemente usarlos (o definir OnlySelected en términos de Required y Pick). (Ejemplo en Playground) (Gracias a caTS por señalar la parte de Required, me había perdido el modificador de bandera y solo mencioné Pick antes.)
