Search code examples
typescriptinterfacetype-inference

Is there a way to match the type to the "key" change?


For example, I wrote the following code.

func({
  items: [
    {
      event: 'a',
      callback: (data) => {

      },
    },
    {
      event: 'b',
      callback: (data) => {

      },
    },
    {
      event: 'c',
      callback: (data) => {

      },
    },
  ],
});

I want is this.

  • When the event is 'a', the data type of the callback factor is inferred to A interface.
  • When the event is 'b', the data type of the callback factor is inferred to B interface.
  • When the event is 'c', the data type of the callback factor is inferred to C interface.

Will it be possible?

The addEventListener seems to be already working, so I tried referring to this part, but it didn't work out.


Solution

  • First you need to define a mapping between the string literal type of the event property and the type of the intended callback parameter data. Luckily TypeScript has an easy way to represent mappings from string literal types to arbitrary types: it's just an object type. Like the following interface:

    interface EventMap {
        a: A;
        b: B;
        c: C;
    }
    

    One we have that, we can define func() as a generic function like this:

    declare function func<T extends (keyof EventMap)[]>(arg: {
        items: [...{ [I in keyof T]: {
            event: T[I],
            callback: (data: EventMap[T[I]]) => void
        } }]
    }): void;
    

    Here the type parameter T represents the tuple of event properties in the items array. So if you call func() the way you show in your example, then we'd want T to be ["a", "b", "c"].

    When you call func(), the compiler looks at the items property of the argument and tries to match it with the type [...{[I in keyof T]: {event: T[I], callback: (data: EventMap[T[I]]) => void }].

    That's a mapped tuple type which has been wrapped in the variadic tuple type [...+] to give the compiler a hint that you want T to be inferred as a tuple and not an unordered array (see microsoft/TypeScript#39094 where it says "the type [...T], where T is an array-like type parameter, can conveniently be used to indicate a preference for inference of tuple types").

    And because {[I in keyof T]: {⋯T[I]⋯}} is a homomorphic mapped type (see What does "homomorphic mapped type" mean?), the compiler will be able to infer T from the elements of items. For each element T[I] at numeric index I of the tuple type T, the corresponding element of items should have an event property of that type T[I], and a callback property whose data callback parameter is of type EventMap[T[I]], which means we index into EventMap with the key T[I]. The intent is that the compiler will infer T[I] from the event property, and then use that to contextually type the callback data parameter.

    Let's test it out:

    func({
        items: [
            {
                event: 'a',
                callback: (data) => {
                    //     ^?(parameter) data: A
                },
            },
            {
                event: 'b',
                callback: (data) => {
                    //     ^?(parameter) data: B
                },
            },
            {
                event: 'c',
                callback: (data) => {
                    //     ^?(parameter) data: C
                },
            },
        ],
    });
    // function func<["a", "b", "c"]>(⋯): void;
    

    Looks good. The compiler inferred ["a", "b", "c"] for T as desired, and the data parameter of each callback is appropriately contextually typed.

    Playground link to code