JHH
JHH

Reputation: 9285

Handling records of generic functions

Let's define the following type for a handler function:

type Handler<T, U> = (x: T) => U;

Then say there's a library that takes a dictionary of handlers (which is then used somehow to route data to a specific handler, how is not relevant to the question):

registerHandlers(handlers: Record<string, Handler<any, any>>) {
  ...
} 

This works fine, and I can e.g. pass a literal object to the registerHandlers function:

registerHandlers({
  foo: (x: number) => 'foo', 
  bar: (x: string) => 42
});

However, to make the interface clearer and to allow for easier stubbing and testing of the handlers, I'd like to define an interface that describes my handlers:

interface Handlers {
  foo: Handler<number, string>;
  bar: Handler<string, number>;
}

This way it's easy to define or override a set of handlers with all types inferred (obviously my example is simpled down, my real code has much more complex types which is why I want to define a clear type for the entire collection of handlers):

const handlers: Handlers = {
  foo: x => 'foo',
  bar: x => 42
}

However, when I try to use my interface as a Record<string, Handler<any, any>> by calling registerHandlers(handlers), it doesn't work, not even if using as Record<...> (unless I do double conversion via unknown which always seems like the last resort):

Index signature is missing in type Handlers

My Handlers interface clearly is an object with a set of Handler functions, so in practice, it should match a Record of the same, but it doesn't. I guess I understand why an arbitrary interface cannot be guaranteed to match a Record, but is there a nice way of defining a type so that it is a Record<string, Handler<any, any>> but still declared as a type where each individual property is a properly typed Handler<, >? Or am I stuck with a double conversion via unknown, with the risks associated (a simple typo could go unnoticed etc)?

Upvotes: 0

Views: 195

Answers (2)

Karol Majewski
Karol Majewski

Reputation: 25790

You can work around the problem by re-defining the argument expected by registerHandler:

type Handler<T, U> = (x: T) => U;

interface Handlers {
  foo: Handler<number, string>;
  bar: Handler<string, number>;
}

const handlers: Handlers = {
  foo: x => 'foo',
  bar: x => 42
}

declare class Foo {
  registerHandlers<T extends string>(handlers: Record<T, Handler<any, any>>): void;
}

new Foo().registerHandlers(handlers);

Upvotes: 1

leetwinski
leetwinski

Reputation: 17859

this is a known issue of interfaces' behavior.

simple workaround would be using type alias instead of interface here, if shouldn't affect your future code, since they are (mostly) interchangeable.

type Handlers = {
  foo: Handler<number, string>;
  bar: Handler<string, number>;
}

ts playground

Upvotes: 1

Related Questions