Search code examples
javascripttypescriptunderscore.jslodashecmascript-2016

TypeScript Type-safe Omit Function


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!


Solution

  • 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