0

I'm trying to create a map of functions for a game I'm making. Basically, I have an Entity base interface and a bunch of others entities (e.g. Monster) extending from it, and I need a map of functions that can have any string as a key and, as a value, a function which accepts a subtype extending Entity as an argument, and has specific code for that particular subentity.

The problem is I need to call some of those functions by its key, but I need to also pass a subtype as argument. Of course, Typescript doesn't allow this due to function params being contravariants. I think I have a decent understanding of the why (but further explanations are always welcomed), what I want to know if there's any way to overcome this limitation.

The following is not my actual code, but an example of what I'm attempting to do:

interface Shape { type: string}

interface Triangle extends Shape { type: 'TRIANGLE'}
interface Square extends Shape { type: 'SQUARE' }
interface Circle extends Shape { type: 'CIRCLE' }

const triangle: Triangle = { type: 'TRIANGLE' }

type Shapes = Triangle | Square | Circle

type FunctionMap = {
    [k: string]: (shape: Shapes) => void
}

const map: FunctionMap = {
  'some-key': (shape: Triangle) => {} // Typescript error here
}

type Key = keyof typeof map

const myFunc = <T extends Shapes>(shape: T, key: Key) => {
  map[key](shape)
}

myFunc(triangle, 'some-key)

And this is the error I'm getting:

Type '(shape: Triangle) => void' is not assignable to type '(shape: Shapes) => void'.
  Types of parameters 'shape' and 'shape' are incompatible.
    Type 'Shapes' is not assignable to type 'Triangle'.
      Type 'Square' is not assignable to type 'Triangle'.
        Types of property 'type' are incompatible.
          Type '"SQUARE"' is not assignable to type '"TRIANGLE"'.

Here's a playground link, just in case.

Of course, I could make the functions to accept an Entity (or Shape in the example) as argument, and use typeguards, but this feels like cheating, pollutes the code, and goes against the whole idea of types. I also have checked this article and several others, without success.

Thanks in advance!

phoc
  • 1
  • 1
  • I can kinda get around the first problem like [this](https://tsplay.dev/mppYbm), but I don't have a clue on how to get the second problem working. – kelsny Oct 30 '22 at 23:33
  • Does [this approach](https://tsplay.dev/wXq8oN) meet your needs? If so I could write up an answer explaining how it works; if not, what am I missing? (Pls mention @jcalz in a comment to notify me if you reply) – jcalz Oct 31 '22 at 02:21
  • Thanks @caTS and @jcalz! Both solutions are amazing and just what I needed. I need to further study your code to fully understand it, but I'm already learning a lot just reading it. @jcalz, an explanation would be amazing, thank you so much. I'm also curious because I have a different but similar issue, but maybe I should write a new question. What if instead of passing a specific subtype, I want to pass the `Shapes` type or the base type `Shape`? Something like `callMyFunc(shape: S, key: K) => myFunc(shape, key)`. – phoc Oct 31 '22 at 13:14

0 Answers0