Ioannis Noukakis
Ioannis Noukakis

Reputation: 361

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:

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.

Upvotes: 2

Views: 117

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1074168

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

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 (but keep reading):

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

Playground example


But, OnlySelected seems like it's doing the same thing the built-in Required and Pick do, so you could just use them(or define OnlySelected in terms of Required and Pick). (Playground example) (Thanks to caTS for pointing out the Required part, I'd missed the flag mod and just mentioned Pick before.)

Upvotes: 1

Related Questions