16

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:

  1. Since key is a keyof T and T is an object, key can be a symbol, number or string.

  2. Since I use the for in loop, key can only be a string but includes might take a number 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
  • 2,849
  • 4
  • 24
  • 33
  • Hey, Salami, I hope I'll get feedback to my answer. – Nurbol Alpysbayev Dec 29 '18 at 11:22
  • @NurbolAlpysbayev Thanks for your answer. I would just like to give the chance for others to perhaps present a simpler solution if it exists. I won't simply assume something simpler exists, but I have a hard time fully understanding your solution to be completely honest :) – Salami Dec 30 '18 at 12:34
  • Nope, there is no simpler solution, I *guarantee* you. Because it *is* simple, but you just haven't get it yet. You could ask me what exactly is not clear for you.. – Nurbol Alpysbayev Dec 30 '18 at 14:27

8 Answers8

31
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.

Playground

Nurbol Alpysbayev
  • 19,522
  • 3
  • 54
  • 89
  • 1
    ESLint complains about `T extends object` but this can be replaced with `T extends Record` and it still works. – Andreas Linnert Feb 09 '21 at 09:00
  • Cheers mate. I didn't know I needed this function so much – Baptiste Arnaud Sep 11 '21 at 07:27
  • Thanks about sample, I rewrote it a little bit to export const copyWithout = , K extends [...(keyof T)[]]>( source: T, ...toExclude: K ) => { return Object.keys(source).filter(key => !toExclude.includes(key)).reduce<{ [K2 in Exclude]: T[K2]; }>((acc, curr) => { return { ...acc, [curr]: source[curr] }; }, {} as { [K in keyof typeof source]: typeof source[K] }); }; – Lonli-Lokli Oct 04 '21 at 11:52
  • The 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? – Per Enström Apr 14 '22 at 09:52
  • Thanks for pointing me in the right direction. Here's a slightly simplified version of this which has less verbose typings: [Playground](https://www.typescriptlang.org/play?#code/MYewdgzgLgBCC2BLWBeGAeAKjApgDyhzABMI4AjAKx2CgBoYBVXAo0mAaxwE8QAzGJgB8ACgBQMGMhzwAXILoSYAOlVduEeYwDaAXTEBKeQG8l2gNJSwMAKJ5gAGwCuxHOnX8FTIbvmYLugDcYgC+MChCMKaSoJCwAE44qFFhAIZkmMFKDkmcPPIeAplKfCDxMCLqVlKE8AZRSpKIAiIAhOoQyohgji44EJU8MOlMBvXRkpOJUNrquuE1MrM8QY0wIUobStNO8dbTwSFZsdAweAsIyCLGw-IARHcM5PIAjAzA93frDHepjzB3ch3AyBIA) – no_stack_dub_sack Sep 29 '22 at 21:26
10

Simplest way:

export const omit = <T extends object, K extends keyof T>(obj: T, ...keys: K[]): Omit<T, K> => {
  keys.forEach((key) => delete obj[key])
  return obj
}

As a pure function:

export const omit = <T extends object, K extends keyof T>(obj: T, ...keys: K[]): Omit<T, K> => {
  const _ = { ...obj }
  keys.forEach((key) => delete _[key])
  return _
}
90dy
  • 101
  • 2
  • 4
2

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;
}
Jeremy Chone
  • 3,079
  • 1
  • 27
  • 28
1

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];
    };
}
Jonas Tomanga
  • 1,069
  • 10
  • 19
  • Very useful. Just a small correction, in convertToNumbers method, the comparison type should be `==` instead of `===` as it will return false("2" === 2). – cvss Jun 21 '21 at 05:02
1

Not sure if I get a point but I came across similar issue that I wanted be sure that I make no typo when omitting properties so I came to solution like this:

export interface Person {
  id: string;
  firstName: string;
  lastName: string;
  password: string;
}

type LimitedDTO<K extends keyof Person> = Omit<Person, K>;

export type PersonDTO = LimitedDTO<"password" | "lastName">;

And tsc will not allow you omit property which is not present on Person interface

Jacek Plesnar
  • 479
  • 3
  • 6
0

If we limit the type of keys to string [],It works. But it does not seem to be a good idea.Keys should be string | number | symbol[];

function omit<T, K extends string>(
  obj: T,
  ...keys: K[]
): { [k in Exclude<keyof T, K>]: T[k] } {
  let ret: any = {};
  Object.keys(obj)
    .filter((key: K) => !keys.includes(key))
    .forEach(key => {
      ret[key] = obj[key];
    });
  return ret;
}
const result = omit({ a: 1, b: 2, c: 3 }, 'a', 'c');
// The compiler inferred result as 
// {
//   b: number;
// }
郭一凡
  • 36
  • 3
  • Yes I don't think it's a good idea either, because then omit won't work for regular arrays. – Salami Dec 29 '18 at 10:34
0

Unfortunately, it is impossible to get rid of as any

const removeProperty = <Obj, Prop extends keyof Obj>(
  obj: Obj,
  prop: Prop
): Omit<Obj, Prop> => {
  const { [prop]: _, ...rest } = obj;

  return rest;
};

export default removeProperty;


const omit = <Obj, Prop extends keyof Obj, Props extends ReadonlyArray<Prop>>(
  obj: Obj,
  props: readonly [...Props]
): Omit<Obj, Props[number]> =>
  props.reduce(removeProperty, obj as any);

Playground

0

Using the array reduce method to omit the props.

const omitProps = <T extends object, K extends keyof T>(
  data: T,
  props: Array<K>
): Omit<T, K> => {
  if (!data || !Array.isArray(props) || !props.length) {
    return data;
  }
  return props.reduce((acc, prop) => {
    const { [prop as keyof object]: prop1, ...rest } = acc;
    return rest;
  }, data);
};
Manish Kumar
  • 1,131
  • 15
  • 28