title: Constrain one Typescript generic parameter based on properties of another?
body: <p>I'm trying to write a function that takes an object and a (string) key, then operates on a property of the object.  This is easy:</p>
  <pre><code>function f&lt;T extends any, K extends keyof T&gt;(obj: T, key: K) {
    const prop = obj[key]; // prop is typed as T[K]
  }
  </code></pre>
  <p>I would like to <em>constrain the key passed to the call</em>, at compile-time, based on the type of <code>T[K]</code>.  I tried this:</p>
  <pre><code>function f&lt;T extends any, K extends keyof T&gt;(obj: T, key: T[K] extends number ? K : never) {
    obj[key] = 5; // error, &quot;is not assignable to&quot; etc
  }
  </code></pre>
  <p><code>prop</code> is typed as <code>T[T[K] extends number ? K : never]</code> which reads to me like it should collapse to just <code>number</code>, but it does not.</p>
  <p>My goal is to be sure that <code>obj[key]</code> is typed as <code>number</code>, inside the function, and also have calls like <code>f({a: true}, &quot;a&quot;)</code> flagged as an error.  Is this possible?  I thought I might need to move the constraint from the function parameter declaration, to the generic parameter declaration, but I couldn't figure out the syntax.</p>
  <hr />
  <p>ETA yet again: <a href="https://www.typescriptlang.org/play?#code/C4TwDgpgBAggznAlgcwHYEMBGAbCBpCEOAHgBUAaKANXWwFcJTwIA+KAXigG8AoKKANoEQURKigBrQgHsAZlFIBdALQB+AFzVaDJpCgQAHsAioAJnAVDCiqAB8odMxFliIpqKqjCom1BABuEABOPAC+AlIgcgqKANw8PKB6AAqIAMYS8EhoWLhklDT0jMxsnKkZ+bAIKBg4+IQkFFpFuqws8YnMUADyqNggWTW5EJWFOiUc3HyC3mJV2bW4wo0F2sWQLIqaY+sQYR1ixkGy6GnQAGLS0lP86L50ALaYwfH8mJpwwEFiyK9QqK5NABOP4AdwAFuhjP44Jp0KgQPFQgkkhcrgA5R7PILLSaDHJ1ZbES7SSioLHBdpQAD01KgACJ0PS7AyIVCAnB6Z09ABheH41CYp7BSblTLVAl5ElkilBKm0-hcKB3f6y2JQNnQ2HKhHq5GoqAAEWcYkQxn6ACk6J8hdiLJxev0BcNiVcZcK5eqFdxlfcPerNRy-di9QlZI40sBENJxLIAIxkFgACmkmAAVppHQMJYsRk1yR6WJRIppItEs866pUC9iWABKG5QVNpiLWSYAWSh4IAdEF4aZpA8k3WvXSBQ8TMBRBZUe40tIgkEIJH+pRkFdTGEwxGozGoLIAExkfRGEzmJvp5fASh4E-GMwWMvyMWVvL52UsZPNzRNEteBu8PwzatiANicJ2wA9n2ZiDsOo5QAAquCTbgpQrRQAA5DWwQYdO-zSFO6A5sMUDANcBoYaQQiKBh3ZJgeADMB4HnWW48KYy7YOgS5QPOqCfPuVyaCS8TxkmshugyTIjjSY7YNg0igm4a4bjwYkSaSrKQlq9IyQqMDyYpylQOu0ibupkn0gCfi6fBSEoZQcDgtIdDYO4wRBAulCYHQU4DhAcCoBhwAAIRqXG4mWZgtmyVAACii5eSZqlqQekWaYyMX6YZSmmCpZmpellD0oGMJZXJCm5fl5lpRpxXWRA5XxYlQTVYVdUMtFel0glnmtclBVAA" rel="nofollow noreferrer">Playground example</a> -- updated to try the approach suggested by @reactgular in a comment:</p>
  <pre><code>type AssignableKeys&lt;T, ValueType&gt; = {
    [Key in keyof T]-?: ValueType extends T[Key] | undefined ? Key : never
  }[keyof T];
  
  type PickAssignable&lt;T, ValueType&gt; = Pick&lt;T, AssignableKeys&lt;T, ValueType&gt;&gt;;
  
  type OnlyAssignable&lt;T, ValueType&gt; = {
    [Key in AssignableKeys&lt;T, ValueType&gt;]: ValueType
  };
  
  interface Foo {
    a: number;
    b: string;
    nine: 9;
    whatevs: any;
  }
  
  type FooNumberKeys = AssignableKeys&lt;Foo, number&gt;; // &quot;a&quot; | &quot;whatevs&quot;
  type CanAssignNumber = PickAssignable&lt;Foo, number&gt;; //  { a: number; whatevs: any; }
  type DefinitelyJustNumbers = OnlyAssignable&lt;Foo, number&gt;; //  { a: number; whatevs: number; }
  
  function f1&lt;T&gt;(obj: OnlyAssignable&lt;T, number&gt;, key: keyof OnlyAssignable&lt;T, number&gt;) {
    obj[key] = Math.random(); // Assignment is typed correctly, good
  }
  
  function f2&lt;T extends object, K extends keyof PickAssignable&lt;T, number&gt;&gt;(obj: T, key: K) {
    obj[key] = Math.random(); // Uh oh, Type 'number' is not assignable to type 'T[K]'.(2322)
  }
  
  declare const foo: Foo;
  f1(foo, &quot;a&quot;); // Allowed, good
  f1(foo, &quot;whatevs&quot;); // Allowed, good
  f1(foo, &quot;nine&quot;); // Uh oh, should error, but doesn't!
  f1(foo, &quot;b&quot;); // Error, good
  
  f2(foo, &quot;a&quot;); // Allowed, good
  f2(foo, &quot;whatevs&quot;); // Allowed, good
  f2(foo, &quot;nine&quot;); // Error, good
  f2(foo, &quot;b&quot;); // Error, good
  </code></pre>
  <p>In the Playground, <code>DefinitelyJustNumbers</code> shows a tooltip of <code>{a: number; whatevs: number}</code> -- anything that I can assign a <code>number</code> to is explicitly typed as <code>number</code>.  This fixes the assignment inside the function body, but fails to detect the fact that <code>nine</code> is only a subset of number and so should not be allowed.</p>
  <p><code>CanAssignNumber</code> shows a tooltip of <code>{a: number; whatevs: any}</code>, correctly excluding <code>nine</code> because it's not assignable to <code>number</code>.  This looks good, but still doesn't fix assignment inside the function <code>f2</code>.</p>