Home > other >  How to express in TypeScript the type of a Record minus some of its key in a generic manner?
How to express in TypeScript the type of a Record minus some of its key in a generic manner?

Time:11-11

I have a function that filters out some keys from a Record and I want the type safety to prevent me from accessing filtered out keys.

What I got to express that is:

// 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
}

Which works great for simple use cases:

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;
//}

But now if I try to use theses types in a generic function I have to means to convert the type of the keys that I want to keep to an union type. So I have to provide the type by hand and it's sad to have to repeat the keys to keep twice just to be type safe:

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)

Anyone knows the solution or a better design?

See the playground here.

CodePudding user response:

You can define filterRecord's TO_KEEP as extends keyof T, then use TO_KEEP[] as the parameter type. TypeScript will infer correctly then:

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>>);
};

Playground example

Note that I did have to add a broadening type assertion within the implementation there (keys as readonly string[]) in order to use includes, but that's harmless.


Side note: Unless you're using it for some other purpose, you can avoid needing OmitNever by changing your definition of OnlySelected slightly:

type OnlySelected<T extends object, V> = {
    [K in keyof T as K extends V ? K : never]-?: T[K];
    // −−−−−−−−−−^^^^^^^^^^^^^^^^^^^^^^^^^^^−−−−−^^^^
};

Playground example


But, I think I'm probably missing the point there, because then OnlySelected seems like it duplicates the built-in Pick. (Playground example)

  • Related