title: Defining a type-safe generic object accessor function in TypeScript
body: <p>I'm trying to write a type definition for a utility function like this:</p>
  
  <pre><code>function getter&lt;T&gt;(field1, field2?, field3?) {
      if (field3 !== undefined) {
          return (obj: T) =&gt; obj[field1][field2][field3];
      } else if (field2 !== undefined) {
          return (obj: T) =&gt; obj[field1][field2];
      } else {
          return (obj: T) =&gt; obj[field1];
      }
  }
  </code></pre>
  
  <p>Such that, given the following interface:</p>
  
  <pre><code>interface Contact { phone: string; }
  interface Author { name: string; contact: Contact; }
  interface Book { id: number; author: Author; }
  </code></pre>
  
  <p>And example instance:</p>
  
  <pre><code>const lordOfTheRings: Book = { id: 1, author: { name: 'JRR Tolkien', contact: { phone: '222222' } } };
  </code></pre>
  
  <p>You could use <code>getter&lt;Book&gt;('author', 'name')</code> to get a function which, when called with the argument <code>lordOfTheRings</code>, would return <code>JRR Tolkien</code>.</p>
  
  <p>So the function above works fine but it's not type safe. I want to be able to write <code>getter&lt;Book&gt;('id')</code> and have it compile happily but if I were to write <code>getter&lt;Book&gt;('banana')</code> have it give me a type error because <code>banana</code> isn't a valid key of <code>Book</code>. I just want this to work up to three levels deep.</p>
  
  <p>I've been round and round on this and struggled to write a type definition that works in all edge cases.</p>
  
  <p>What I can do is something like this:</p>
  
  <pre><code>interface Getter&lt;T&gt; {
      &lt;K extends keyof T&gt;(field1: K): (obj: T) =&gt; T[K];
      &lt;K extends keyof T, L extends keyof T[K]&gt;(field1: K, field2: L): (obj: T) =&gt; T[K][L];
      &lt;K extends keyof T, L extends keyof T[K], M extends keyof T[K][L]&gt;(field1: K, field2: L, field3: M): (obj: T) =&gt; T[K][L][M];
  }
  
  const bookGetter: Getter&lt;Book&gt; = getter;
  </code></pre>
  
  <p>Which I can use like so:</p>
  
  <pre><code>bookGetter('id')              // No error
  bookGetter('title')           // Error
  bookGetter('author')(lordOfTheRings).name; // No error
  bookGetter('author', 'name'); // No error
  bookGetter('author', 'age');  // Error
  bookGetter('author', 'contact')(lordOfTheRings).phone;  // No error
  bookGetter('author', 'contact')(lordOfTheRings).email;  // Error
  bookGetter('author', 'contact', 'phone');  // No error
  bookGetter('author', 'contact', 'email');  // Error
  </code></pre>
  
  <p>This is the perfect behaviour in terms of the compiler, but I'd like to be able to specify the type as a parameter to the getter, rather than having to define a particular instance of a getter like this.</p>
  
  <p>I tried this signature to the function:</p>
  
  <pre><code>function getter&lt;T,
                  K extends keyof T = keyof T,
                  L extends keyof T[K] = keyof T[K],
                  M extends keyof T[K][L] = keyof T[K][L]&gt;(field1: K, field2?: L, field3?: M) {
  </code></pre>
  
  <p>Which initially seemed promising but fails on the specification like so:</p>
  
  <pre><code>getter&lt;Book&gt;('id');    // No error - correct
  getter&lt;Book&gt;('title'); // Error - correct
  getter&lt;Book&gt;('author')(lordOfTheRings).name; // Error - NOT correct (property name does not exist on number | Author)
  getter&lt;Book&gt;('author', 'name'); // All the rest also error in the same way as this one (argument of type not assignable to parameter of type never)
  </code></pre>
  
  <p>Please don't worry about the practical utility of this function - at this point it's become of of an intellectual exercise in type specification! Can we crack it?</p>
  
  <p><a href="http://www.typescriptlang.org/play/#src=interface%20Getter%3CT%3E%20%7B%0D%0A%20%20%3CK%20extends%20keyof%20T%3E(field1%3A%20K)%3A%20(obj%3A%20T)%20%3D%3E%20T%5BK%5D%3B%0D%0A%20%20%3CK%20extends%20keyof%20T%2C%20L%20extends%20keyof%20T%5BK%5D%3E(field1%3A%20K%2C%20field2%3A%20L)%3A%20(obj%3A%20T)%20%3D%3E%20T%5BK%5D%5BL%5D%3B%0D%0A%20%20%3CK%20extends%20keyof%20T%2C%20L%20extends%20keyof%20T%5BK%5D%2C%20M%20extends%20keyof%20T%5BK%5D%5BL%5D%3E(field1%3A%20K%2C%20field2%3A%20L%2C%20field3%3A%20M)%3A%20(obj%3A%20T)%20%3D%3E%20T%5BK%5D%5BL%5D%5BM%5D%3B%0D%0A%7D%0D%0A%0D%0A%2F%2F%20function%20getter%3CT%2C%20K%20extendIs%20infer%20F%20%3F%20F%20%3A%20keyof%20T%3E(field1%3A%20F)%3A%20(obj%3A%20T)%20%3D%3E%20T%5BF%5D%3B%0D%0A%2F%2F%20function%20getter%3CT%2C%20K%20extends%20keyof%20T%20%3D%20keyof%20T%2C%20L%20extends%20Keys%3CT%5BK%5D%3E%20%3D%20Keys%3CT%5BK%5D%3E%3E(field1%3A%20K%2C%20field2%3A%20L)%3A%20(obj%3A%20T)%20%3D%3E%20T%5BK%5D%5BL%5D%3B%0D%0A%2F%2F%20function%20getter%3CT%2C%20K%20extends%20keyof%20T%20%3D%20keyof%20T%2C%20L%20extends%20Keys%3CT%5BK%5D%3E%20%3D%20Keys%3CT%5BK%5D%3E%2C%0D%0A%2F%2F%20M%20extends%20Keys%3CT%5BK%5D%5BL%5D%3E%20%3D%20Keys%3CT%5BK%5D%5BL%5D%3E%3E(field1%3A%20K%2C%20field2%3A%20L%2C%20field3%3A%20M)%3A%20(obj%3A%20T)%20%3D%3E%20T%5BK%5D%5BL%5D%5BM%5D%3B%0D%0Afunction%20getter%3CT%2C%0D%0A%20%20K%20extends%20keyof%20T%20%3D%20keyof%20T%2C%0D%0A%20%20L%20extends%20keyof%20T%5BK%5D%3D%20keyof%20T%5BK%5D%2C%0D%0A%20%20M%20extends%20keyof%20T%5BK%5D%5BL%5D%3D%20keyof%20T%5BK%5D%5BL%5D%3E(field1%3A%20K%2C%20field2%3F%3A%20L%2C%20field3%3F%3A%20M)%20%7B%0D%0A%20%20if%20(field3%20!%3D%3D%20undefined)%20%7B%0D%0A%20%20%20%20return%20(obj%3A%20T)%20%3D%3E%20obj%5Bfield1%5D%5Bfield2%5D%5Bfield3%5D%3B%0D%0A%20%20%7D%20else%20if%20(field2%20!%3D%3D%20undefined)%20%7B%0D%0A%20%20%20%20return%20(obj%3A%20T)%20%3D%3E%20obj%5Bfield1%5D%5Bfield2%5D%3B%0D%0A%20%20%7D%20else%20%7B%0D%0A%20%20%20%20return%20(obj%3A%20T)%20%3D%3E%20obj%5Bfield1%5D%3B%0D%0A%20%20%7D%0D%0A%7D%0D%0A%0D%0A%2F%2F%20----%20Example%20Usage%0D%0A%0D%0Ainterface%20Contact%20%7B%20phone%3A%20string%3B%20%7D%0D%0Ainterface%20Author%20%7B%20name%3A%20string%3B%20contact%3A%20Contact%3B%20%7D%0D%0Ainterface%20Book%20%7B%20id%3A%20number%3B%20author%3A%20Author%3B%20%7D%0D%0A%0D%0Aconst%20bookGetter%3A%20Getter%3CBook%3E%20%3D%20getter%3B%0D%0A%0D%0Aconst%20lordOfTheRings%3A%20Book%20%3D%0D%0A%20%20%7B%20id%3A%201%2C%20author%3A%20%7B%20name%3A%20&#39;JRR%20Tolkien&#39;%2C%20contact%3A%20%7B%20phone%3A%20&#39;222222&#39;%20%7D%20%7D%20%7D%3B%0D%0A%0D%0Agetter%3CBook%3E(&#39;id&#39;)(lordOfTheRings).name%3B%0D%0Agetter%3CBook%3E(&#39;bones&#39;)%3B%0D%0A%0D%0Agetter%3CBook%3E(&#39;id&#39;)%3B%20%20%20%20%2F%2F%20No%20error%20-%20correct%0D%0Agetter%3CBook%3E(&#39;title&#39;)%3B%20%2F%2F%20Error%20-%20correct%0D%0Agetter%3CBook%3E(&#39;author&#39;)(lordOfTheRings).name%3B%20%2F%2F%20Error%20-%20NOT%20correct%20(property%20name%20does%20not%20exist%20on%20number%20%7C%20Author)%0D%0Agetter%3CBook%3E(&#39;author&#39;%2C%20&#39;name&#39;)%3B%20%2F%2F%20All%20the%20rest%20also%20error%20(argument%20of%20type%20not%20assignable%20to%20parameter%20of%20type%20never)%0D%0Agetter%3CBook%3E(&#39;author&#39;%2C%20&#39;age&#39;)%3B%0D%0Agetter%3CBook%3E(&#39;author&#39;%2C%20&#39;contact&#39;)(lordOfTheRings).phone%3B%0D%0Agetter%3CBook%3E(&#39;author&#39;%2C%20&#39;contact&#39;)(lordOfTheRings).email%3B%0D%0Agetter%3CBook%3E(&#39;author&#39;%2C%20&#39;contact&#39;%2C%20&#39;phone&#39;)%3B%0D%0Agetter%3CBook%3E(&#39;author&#39;%2C%20&#39;contact&#39;%2C%20&#39;email&#39;)%3B%0D%0A%0D%0AbookGetter(&#39;id&#39;)%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20No%20error%0D%0AbookGetter(&#39;title&#39;)%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Error%0D%0AbookGetter(&#39;author&#39;)(lordOfTheRings).name%3B%20%2F%2F%20No%20error%0D%0AbookGetter(&#39;author&#39;%2C%20&#39;name&#39;)%3B%20%2F%2F%20No%20error%0D%0AbookGetter(&#39;author&#39;%2C%20&#39;age&#39;)%3B%20%20%2F%2F%20Error%0D%0AbookGetter(&#39;author&#39;%2C%20&#39;contact&#39;)(lordOfTheRings).phone%3B%20%20%2F%2F%20No%20error%0D%0AbookGetter(&#39;author&#39;%2C%20&#39;contact&#39;)(lordOfTheRings).email%3B%20%20%2F%2F%20Error%0D%0AbookGetter(&#39;author&#39;%2C%20&#39;contact&#39;%2C%20&#39;phone&#39;)%3B%20%20%2F%2F%20No%20error%0D%0AbookGetter(&#39;author&#39;%2C%20&#39;contact&#39;%25" rel="noreferrer">Click here to go to an example TS playground with these in</a>.</p>
  
  <p>Using TypeScript 3.1.4.</p>