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>>);
};
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];
// −−−−−−−−−−^^^^^^^^^^^^^^^^^^^^^^^^^^^−−−−−^^^^
};
But, I think I'm probably missing the point there, because then OnlySelected
seems like it duplicates the built-in Pick
. (Playground example)