Reputation: 9285
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
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
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>;
}
Upvotes: 1