1
votes

This piece works in TypeScript 2.6:

function resolver<Key extends keyof HashType>(a: Command<Key>): HashType[Key]['out'] {
  return handlers[a.kind](a);
}


const handlers: {[k in keyof HashType]: (arg: Command<k>) => HashType[k]['out']} = {
  a: arg => 1,
  b: arg => ''
};

type Command<Key extends keyof HashType> = HashType[Key]['in'] & { kind: Key }


type HashType = {
  a: { in: { someString: string }, out: number }
  b: { in: { someNumber: number }, out: string }
}

However, since 2.7, it fails with:

TS2349: Cannot invoke an expression whose type lacks a call signature. Type '((arg: Command<"a">) => number) | ((arg: Command<"b">) => string)' has no compatible call signatures.

Here's the same thing in a playground.

1

1 Answers

1
votes

I don't have a great handle on why this worked in TypeScript 2.6, but the reason this fails now is because the compiler is protecting you against something unlikely to happen, and in any case isn't smart enough to realize that the type of handlers[a.kind] is correlated with a.

Consider the following valid but obnoxious code:

const resolved = resolver<"a" | "b">({ someString: "whoops", kind: "b" });

Since Key extends keyof HashType, Key can be equal to keyof HashType. And note that the argument given is a Command<keyof HashType> even though it is neither a Command<"a"> or a Command<"b">. The compiler cannot guarantee that that handlers[a.kind] will be applicable to a.

Is that likely to be an issue in real-life use of the code? Probably not. If not, you can claim that you know more than the compiler and use a type assertion:

function resolver<Key extends keyof HashType>(a: Command<Key>): HashType[Key]['out'] {
  const handler = handlers[a.kind] as (arg: Command<Key>) => HashType[Key]['out'];
  return handler(a);
}

Now the code compiles happily. If you are worried about someone passing a too-wide argument to the code, there are ways around it. But it's probably not worth it.

Hope that helps! For a similar issue, see this question.