1

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.

Daniel Birowsky Popeski
  • 8,752
  • 12
  • 60
  • 125

1 Answers1

1

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.

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Considering how this (mapping commands to handlers) has been a major pattern in my application, I would kindly ask you to help me create an abstraction that would let me keep my current pattern without cheating with assertions. Something that would let me declare that the resolver's generic cannot be a union. – Daniel Birowsky Popeski May 31 '18 at 18:16
  • I can't come up with an abstraction that will make the compiler accept that code without the equivalent of a type assertion (more permissive overload signatures, user-defined type guards, etc). Type assertions are not cheating when you legitimately know more than the compiler, but I understand your point. If you think that your pattern is common/reasonable enough that it should be better supported in TypeScript, you might want to raise an [issue in GitHub](https://github.com/Microsoft/TypeScript/issues) or find a relevant existing one (I haven't found one yet). – jcalz May 31 '18 at 22:43
  • I just explored a bit about conditional types and hoped that something like `type NoUnion = Key extends 'a' ? 'a' : Key extends 'b' ? 'b' : never;` would work, and then I tried yours: `type NotAUnion = [T] extends [infer U] ? T extends any ? [U] extends [T] ? U : never : never : never` neither worked. Both accept `'a' | 'b'`. Could you elaborate on this? Or, can you point me to a resource on conditional types in relationship to unions? – Daniel Birowsky Popeski Jun 01 '18 at 08:10
  • Use [this question](https://stackoverflow.com/q/50639496/592641) it might be more comfortable. – Daniel Birowsky Popeski Jun 01 '18 at 08:52
  • Yeah I see that it doesn’t work (I think the TS version I was using was before some fixes ). But the UnionToIntersection method in the answer to your new question will work. – jcalz Jun 01 '18 at 10:13
  • 1
    Added an answer with a fixed version, in case it matters. – jcalz Jun 01 '18 at 13:46
  • And yeah, unfortunately `NotAUnion<>` will only guard the calling side, not the implementing side. The compiler will still not understand that the implementation is safe and you'll have to use something like a type assertion. – jcalz Jun 01 '18 at 13:48