TypeScript Type-safe Omit Function
Solution 1
interface Omit {
<T extends object, K extends [...(keyof T)[]]>
(obj: T, ...keys: K): {
[K2 in Exclude<keyof T, K[number]>]: T[K2]
}
}
const omit: Omit = (obj, ...keys) => {
const ret = {} as {
[K in keyof typeof obj]: (typeof obj)[K]
};
let key: keyof typeof obj;
for (key in obj) {
if (!(keys.includes(key))) {
ret[key] = obj[key];
}
}
return ret;
};
For convenience I've pulled most of the typings to an interface.
The problem was that K
had been being inferred as a tuple, not as a union of keys. Hence, I changed it's type constraint accordingly:
[...(keyof T)[]] // which can be broke down to:
keyof T // a union of keys of T
(keyof T)[] // an array containing keys of T
[...X] // a tuple that contains X (zero or more arrays like the described one above)
Then, we need to transform the tuple K
to a union (in order to Exclude
it from keyof T
). It is done with K[number]
, which is I guess is self-explaining, it's the same as T[keyof T]
creating a union of values of T
.
Solution 2
Object.keys
or for in
returns keys as string and excludes symbols. Numeric keys are also converted to strings.
You need to convert numeric string keys to numbers otherwise it will return the object with string keys.
function omit<T extends Record<string | number, T['']>,
K extends [...(keyof T)[]]>(
obj: T,
...keys: K
): { [P in Exclude<keyof T, K[number]>]: T[P] } {
return (Object.keys(obj)
.map((key) => convertToNumbers(keys, key)) as Array<keyof T>)
.filter((key) => !keys.includes(key))
.reduce((agg, key) => ({ ...agg, [key]: obj[key] }), {}) as {
[P in Exclude<keyof T, K[number]>]: T[P];
};
}
function convertToNumbers(
keys: Array<string | number | symbol>,
value: string | number
): number | string {
if (!isNaN(Number(value)) && keys.some((v) => v === Number(value))) {
return Number(value);
}
return value;
}
// without converToNumbers omit({1:1,2:'2'}, 1) will return {'1':1, '2':'2'}
// Specifying a numeric string instead of a number will fail in Typescript
To include symbols you can use the code below.
function omit<T, K extends [...(keyof T)[]]>(
obj: T,
...keys: K
): { [P in Exclude<keyof T, K[number]>]: T[P] } {
return (Object.getOwnPropertySymbols(obj) as Array<keyof T>)
.concat(Object.keys(obj)
.map((key) => convertToNumbers(keys, key)) as Array<keyof T>)
.filter((key) => !keys.includes(key))
.reduce((agg, key) => ({ ...agg, [key]: obj[key] }), {}) as {
[P in Exclude<keyof T, K[number]>]: T[P];
};
}
Solution 3
The accepted answer from Nurbol above is probably the more typed version, but here is what I am doing in my utils-min
.
It uses the typescript built-in Omit and is designed to only support string key names. (still need to loosen up the Set to Set, but everything else seems to work nicely)
export function omit<T extends object, K extends Extract<keyof T, string>>(obj: T, ...keys: K[]): Omit<T, K> {
let ret: any = {};
const excludeSet: Set<string> = new Set(keys);
// TS-NOTE: Set<K> makes the obj[key] type check fail. So, loosing typing here.
for (let key in obj) {
if (!excludeSet.has(key)) {
ret[key] = obj[key];
}
}
return ret;
}
Comments
-
Salami almost 2 years
I want to replicate lodash's
_.omit
function in plain typescript.omit
should return an object with certain properties removed specified via parameters after the object parameter which comes first.Here is my best attempt:
function omit<T extends object, K extends keyof T>(obj: T, ...keys: K[]): {[k in Exclude<keyof T, K>]: T[k]} { let ret: any = {}; let key: keyof T; for (key in obj) { if (!(keys.includes(key))) { ret[key] = obj[key]; } } return ret; }
Which gives me this error:
Argument of type 'keyof T' is not assignable to parameter of type 'K'. Type 'string | number | symbol' is not assignable to type 'K'. Type 'string' is not assignable to type 'K'.ts(2345) let key: keyof T
My interpretation of the error is that:
Since key is a
keyof T
andT
is an object, key can be asymbol
,number
orstring
.Since I use the
for in
loop, key can only be astring
butincludes
might take anumber
if I pass in an array, for example? I think. So that means there's a type error here?
Any insights as to why this doesn't work and how to make it work are appreciated!
-
Salami over 5 yearsYes I don't think it's a good idea either, because then omit won't work for regular arrays.
-
Andreas Linnert about 3 yearsESLint complains about
T extends object
but this can be replaced withT extends Record<string, unknown>
and it still works. -
cvss almost 3 yearsVery useful. Just a small correction, in convertToNumbers method, the comparison type should be
==
instead of===
as it will return false("2" === 2). -
Baptiste Arnaud over 2 yearsCheers mate. I didn't know I needed this function so much 🥰
-
Lonli-Lokli over 2 yearsThanks about sample, I rewrote it a little bit to export const copyWithout = <T extends Record<string, unknown>, K extends [...(keyof T)[]]>( source: T, ...toExclude: K ) => { return Object.keys(source).filter(key => !toExclude.includes(key)).reduce<{ [K2 in Exclude<keyof T, K[number]>]: T[K2]; }>((acc, curr) => { return { ...acc, [curr]: source[curr] }; }, {} as { [K in keyof typeof source]: typeof source[K] }); };
-
Per Enström about 2 yearsThe resulting type when calling this omit function is just an empty interface
{}
, not the type I'm expecting. Is this expected, or is there a workaround for that?