0
type Tail<T extends any[]> = ((...t: T) => void) extends ((
  h: any,
  ...r: infer R
) => void)
  ? R
  : never;

type DeepOmit<T, Path extends string[]> = T extends object
  ? {
      0: Omit<T, Path[0]>;
      1: {
        [K in keyof T]: K extends Path[0] ? DeepOmit<T[K], Tail<Path>> : T[K];
      };
    }[Path['length'] extends 1 ? 0 : 1]
  : T;

I have the above type which works well. I have tested it this way:

type A = DeepOmit<{ a: { b: { c: 1 } } }, ['a', 'b', 'c']>;

// {
//    a: {
//        b: Pick<{
//            c: 1;
//        }, never>;
//    };
// }

However when I add this inside a function, I dont get the desired output.

const del = <T extends object, K extends string[]>(src: T, path: K) => {
  // custom logic removed. just creating the result below.
  const result = {
    a: {
      b: {}
    }
  }
  return result as DeepOmit<T, K>
};

const deletedObj = del({a:{b:{c:1}}},["a","b","c"]);
// deletedObj.a.b.c <========== still works
// expected is deletedObj.a.b

Can someone point me what is the issue here ?

Abhishek Saha
  • 2,564
  • 1
  • 19
  • 29

2 Answers2

5

The problem you're facing is that the compiler tends to widen tuples to arrays and literals to nonliterals unless you give it particular hints. The caller of del() could use an as const assertion on the path parameter to get this effect (although you'd need to accept readonly string[] and not just string[]), but you presumably would like to have the caller not worry about it. I've filed microsoft/TypeScript#30680 to ask for something like as const on the call signature to do this. In the absence of such a feature, you have to rely on some weird hints.

If you want an array type to be inferred as a tuple if possible, you should include a tuple type in its context, such as via a union. So T extends Foo[] becomes T extends Foo[] | []. If you want a type to be inferred as a string literal if possible, you should include a string literal in its context or some other string-like hint. So T extends string[] becomes either T extends (string | (""&{__:0}))[] or, my preference, T extends N[] where N is another generic parameter N extends string. Here's how it looks:

declare const del: <T extends object, K extends N[] | [], N extends string>(
    src: T, path: K
) => DeepOmit<T, K>;

And you can see it work as you intended:

const deletedObj = del({ a: { b: { c: 1 } } }, ["a", "b", "c"]);
deletedObj.a.b; // okay
deletedObj.a.b.c; // error

Also note that your definition of DeepOmit is unnecessarily roundabout; the {0: X, 1: Y}[T extends U ? 0 : 1] technique is a way of circumventing errors when T extends U ? X : Y is considered by the compiler to be circular (even though this sort of sidestepping is not supported). But the straightforward implementation of DeepOmit doesn't run afoul of those rules: it's fine to have a recursive type in which the recursion happens down in an object property:

type DeepOmit<T, Path extends string[]> = T extends object ?
    Path['length'] extends 1 ? Omit<T, Path[0]> : {
        [K in keyof T]: K extends Path[0] ? DeepOmit<T[K], Tail<Path>> : T[K];
    } : T; // no error, still works

Okay, hope that helps; good luck!

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • That's the correct one. I forgot about that we can take out the element type and inference will get better. – Maciej Sikora Mar 04 '20 at 17:01
  • @jcalz thank you for your answer. The problem that I am facing is that all the methods are inside an interface. I understood the problem that you described and went to github to read more about it. Since I cannot declare const inside an interface, is their any other way I can achieve this ? – Abhishek Saha Mar 04 '20 at 18:35
2

The issue is related with type inference of the second argument. In the type example type is strict as ['a','b','c'] but in the function argument is infered as string[], so your type level function DeepOmit is called with string[] not with ['a','b','c']. The result is the same as:

type Result = DeepOmit<{ a: { b: { c: 1 } } }, string[]>;

In order to fix it we need to or strictly type the variable by saying ['a','b','c'] as ['a','b','c'] which is very verbose, or we can make the function be more strict by treating every argument separately (using spread). The downside is that API of the function will change (I would say even for better ) a little bit, but inference works now as expected. Consider:

const del = <T extends object, K extends string[]>(src: T, ...path: K) => {
  // custom logic removed. just creating the result below.
  const result = {
    a: {
      b: {}
    }
  }
  return result as DeepOmit<T, K>
};

const deletedObj = del({ a: { b: { c: 1 } } }, 'a', 'b', 'c');
deletedObj.a.b.c // error no c !
Maciej Sikora
  • 19,374
  • 4
  • 49
  • 50
  • Thanks Maciej. This works great, however, it is not an option for me to change the API. There are various methods which have the similar signature, so dont want to make an exception for this method. – Abhishek Saha Mar 04 '20 at 16:16