title: Typescript - type safe deep omit, or: how to list legal object paths
body: <p>Alright, this is a long and very specific one. Would greatly appreciate any input.</p>
  
  <p>I've written a recursive <code>Omit</code> type, which takes a type <code>T</code> and a tuple of strings (a 'path'), and indexes into <code>T</code>, removing the last item on the path and returning that type. </p>
  
  <p>I've already looked at <a href="https://stackoverflow.com/questions/55539387/deep-omit-with-typescript">Deep Omit With Typescript</a> - but that deals with recursively omitting the same key rather than traversing along a path.</p>
  
  <p>I've also looked at <a href="https://stackoverflow.com/questions/53966509/typescript-type-safe-omit-function">Type Safe Omit Function</a> but that deals with run-time and is about a one-level omit.</p>
  
  <p>My implementation:</p>
  
  <pre class="lang-js prettyprint-override"><code>// for going along the tuple that is the path
  type Tail&lt;T extends any[]&gt; = ((...args: T) =&gt; void) extends ((head: unknown, ...rest: infer U) =&gt; void) ? U : never;
  
  type DeepOmit&lt;T, Path extends string[]&gt; = T extends object ? {
      0: Omit&lt;T, Path[0]&gt;;
      1: { [K in keyof T]: K extends Path[0] ? DeepOmit&lt;T[K], Tail&lt;Path&gt;&gt; : T[K] }
  }[Path['length'] extends 1 ? 0 : 1] : T;
  </code></pre>
  
  <p>I want to make it so <code>Path</code> is constrained to be a valid traversal of <code>T</code>, something like:</p>
  
  <pre><code>Path = [K1, K2, K3, ..., K_n] such that:
  K1 extends keyof T, K2 extends keyof T[K1], ... Kn extends keyof[T][K1]...[K_(n-1)]
  </code></pre>
  
  <p><strong>First attempt</strong></p>
  
  <p>I've written a type that takes a type <code>T</code> and a path <code>P</code> and returns <code>P</code> if it's valid, or <code>never</code> otherwise:</p>
  
  <pre class="lang-js prettyprint-override"><code>type ExistingPathInObject&lt;T, P extends any[]&gt; = P extends [] ? P : {
      0: ExistingPathInObject&lt;T[P[0]], Tail&lt;P&gt;&gt; extends never ? never : P;
      1: never;
  }[P[0] extends keyof T ? 0 : 1]
  </code></pre>
  
  <p>However, in <code>DeepOmit</code>'s signature I can't enforce <code>Path extends ExistingPathInObject&lt;T,Path&gt;</code> as that is a circular dependency. I mention this attempt because there might be a way to circumvent the circularity and use this type to verify <code>Path</code> as a valid traversal of <code>T</code>.</p>
  
  <p><strong>Second Attempt</strong></p>
  
  <p>Because I can't seem to use <code>Path</code> to constrain itself, I instead tried to generate a union of all existing paths in a type and then require <code>Path</code> to extend that. The best I could come up with is this:</p>
  
  <pre class="lang-js prettyprint-override"><code>type Paths&lt;T&gt; = {
      0: { [K in keyof T]: T[K] extends object ? [K, Paths&lt;T[K]&gt;[keyof Paths&lt;T[K]&gt;]] : [K] }
      1: []
  }[T extends object ? 0 : 1];
  
  // an example type to test on
  type HasNested = { a: string; b: { c: number; d: string } };
  
  type pathTest = Paths&lt;HasNested&gt;;
  //{
  //  a: ["a"];
  //  b: ["b", ["d"] | ["c"]];
  //}
  
  type pathTestUnion = Paths&lt;HasNested&gt;[keyof Paths&lt;HasNested&gt;]
  // ["a"] | ["b", ["d"] | ["c"]]
  </code></pre>
  
  <p>This lets me match a path written as a tree: <code>['a'] extends pathTestUnion</code>, and <code>['b', ['d']] extends pathTestUnion</code> are both true. I have to add a <code>[keyof T]</code> to get the union and I can't put it in the type <code>Paths</code> itself because it's not recognized as valid.</p>
  
  <p>Having done all of that, I'm now having difficulty rewriting <code>DeepOmit</code> to use this constraint. Here's what I've tried:</p>
  
  <pre class="lang-js prettyprint-override"><code>type Types&lt;T&gt; = T[keyof T];
  
  type TypeSafeDeepOmit&lt;T, Path extends Types&lt;Paths&lt;T&gt;&gt;, K extends keyof T&gt; =
      Path extends any[] ?
      T extends object ?
      { // T is object and Path is any[]
          0: Omit&lt;T, K&gt;;
          1: { [P in keyof T]: P extends Path[0] ? TypeSafeDeepOmit&lt;T[P], Path[1], Path[1][0]&gt; : T[P] }
      }[Path['length'] extends 1 ? 0 : 1] :
      T : // if T is not object
      never; // if Path is not any[]
  
  type TSDO_Helper&lt;T, P extends Types&lt;Paths&lt;T&gt;&gt;&gt; = P extends any[] ? P[0] extends keyof T ? TypeSafeDeepOmit&lt;T, P, P[0]&gt; : never : never;
  </code></pre>
  
  <p>This is ugly and uses a helper type to actually work. I also have to tell the compiler that <code>P extends any[]</code> and <code>P[0] extends keyof T</code>, even though that is exactly what <code>Paths</code> is meant to ensure. I'm also getting an error in the recursive call of <code>TypeSafeDeepOmit</code> using <code>Path[1]</code> - </p>
  
  <pre><code>Type 'any[] &amp; Path' is not assignable to type '[]'.
          Types of property 'length' are incompatible.
            Type 'number' is not assignable to type '0'
  </code></pre>
  
  <p>I've fixed that by setting <code>Path extends Types&lt;Paths&lt;T&gt;&gt; | []</code> but I'm not sure that's the right way to go about it.</p>
  
  <p><strong>In summary</strong></p>
  
  <p>So, is there any nicer way to enforce a valid path? Is it possible to also support a union of paths, so as to omit all of them? Right now the result I get is a union of the results of different omits.</p>