Search code examples
typescriptgeneric-type-parameters

Inferring generic parameters from generic types


I'm running into an issue when trying to infer generic parameters on generic types that are specifically objects:

type Box <T extends object> = { value: T; }

function set <T extends object>(box: Box <T> , newValue: T): void {
  box.value = newValue;
}

const bbox: Box <{ foo: string }> = {
  value: {
    foo: "bar"
  }
};

set(bbox, {
  foo: "baz"
}); // OK as expected
set(bbox, 42); // ERR as expected
set(bbox, {
  bar: "baz"
}); // ERR as expected

set(bbox, {}); // OK, unexpected
set(bbox, { /* no autocomplete/intellisense is given here for valid props */ });

If you provide {} as the parameter, TypeScript infers {} as a valid type for the function. This also means that in an editor, no autocomplete is given for the fields of the specified object type, since the {} matches all objects.

Is there an order as to how TypeScript infers generic parameters (i.e. newValue's inference overrides Box<T>'s inference, and any object is assignable to {})? If so, is there a way to avoid this?


You can partially solve this issue by adding an extra parameter:

function set <T, TN extends T> (box: Box <T> , newValue: TN): T {
  box.value = newValue;
  return newValue;
}

const bbox: Box <{ foo: string }> = {
  value: {
    foo: "bar"
  }
};

set(bbox, {}); // ERR, expected
set(bbox, { /* still no autocomplete/intellisense */ });

However, you still do not receive any autocomplete/IntelliSense. I would guess it's because of the extends, and how you're no longer looking for T directly.


Solution

  • I don't know enough about how autocomplete works to tell you exactly why it doesn't work in your case; my intuition is that it hasn't fully committed to the generic type inference. One workaround that forces it to commit to the inferred type is to change set into a curried function like this:

    const intelliSet = <B extends Box<{}>>(box: B) => 
      (newValue: B['value']) => set(box, newValue);
    
    intelliSet(bbox)( { foo: 'bar' }); // note the second function call
    intelliSet(bbox)( { /* autocomplete works here */ });
    

    I don't know if there are non-workaround solutions. I'd be inclined to file a suggestion issue on GitHub, if one doesn't already exist. This one is possibly relevant, especially this comment. Anyway, good luck!