0

Consider the following Typescript code:

class OrderFixture {
  orderId: string;

  constructor() {
    this.orderId = "foo";
  }
}

class DecisionFixture {
  decisionId: string;

  constructor() {
    this.decisionId = "bar";
  }
}

class FixtureStore {
  order = () => new OrderFixture();
  decision = () => new DecisionFixture();
}

const fixtureStore = new FixtureStore();

export function getFixture<
  K extends keyof FixtureStore,
  T extends ReturnType<FixtureStore[K]>
>(entityName: K): T {
  return fixtureStore[entityName](); // ERROR: Type 'OrderFixture' is not assignable to type 'T'.
}

It yields the following type error:

Type 'OrderFixture | DecisionFixture' is not assignable to type 'T'.
  'OrderFixture | DecisionFixture' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'OrderFixture | DecisionFixture'.
    Type 'OrderFixture' is not assignable to type 'T'.
      'OrderFixture' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'OrderFixture | DecisionFixture'.ts(2322)

Here is a playground.

There seems to be a fairly canonical answer to the question raised by this type of error but I am unable to see any relevant similarity between the two causes laid out there and my code.

If I force cast the return value to T as suggested in this answer I get the right types when invoking getFixture. Why doesn't Typescript infer these types for me?

303
  • 888
  • 1
  • 11
  • 31
  • No expect on TS, but I would have expected `function getFixture(entityName:keyof FixtureStore){ return fixtureStore[entityName](); }` to also be able to infer the type of either OrderFixture or DecisionFixture as there is enough info for the compiler to work it out. – Keith Jan 07 '21 at 12:22
  • This is a great question, by the way, I've had similar issues with this before. I would say its because the inference goes as far as ```OrderFixture | DecisionFixture``` yet you would only spec one of them so you have to say which one of the two it is using the type casting... – bloo Jan 07 '21 at 12:27
  • What's even weirder if you cast as `any`, the callee still gets the correct inferred type. mmmm – Keith Jan 07 '21 at 21:23

1 Answers1

1

Typescript's error message here is perfectly descriptive: your function returns something which is assignable to OrderFixture | DecisionFixture, but not assignable to an arbitrary subtype of it, such as e.g. OrderFixture & HTMLCanvasElement. If you still aren't sure, consider the following code, in which your function promises to return such a thing; it is clearly not type-safe, but it has no error, because it's only using the return type that your function claims to have.

let canvas = getFixture<'order', OrderFixture & HTMLCanvasElement>('order');
let ctx = canvas.getContext('2d');

Normally, for exactly this reason, you should not have a type parameter which only appears in the return position, because it allows the caller to expect a specific type without passing any argument that allows the function to know what type it is expected to return. In your case, there is no need for the type parameter T: just make ReturnType<FixtureStore[K]> the return type of your function directly.


That said, in this case there is something fishy going on that does seem to be Typescript's fault: even splitting up the function's logic and providing type annotations to help the compiler, Typescript still gives an error.

function getFixture<K extends keyof FixtureStore>(entityName: K): ReturnType<FixtureStore[K]> {
  let factory: FixtureStore[K] = fixtureStore[entityName];

  // error here
  let instance: ReturnType<typeof factory> = factory();
  
  return instance;
}}

Playground Link

Logically, Typescript should never complain that the type of factory() might not be assignable to ReturnType<typeof factory>, yet here it complains of exactly that. So I think this should be looked at by someone who knows about Typescript's internals, and perhaps raised as a bug on their issue tracker.

kaya3
  • 47,440
  • 4
  • 68
  • 97