Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

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

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:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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.

>Solution :

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)

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading