Búsqueda de sitios web

¿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.)