TypeScript method Overloads

Viewed 87

I have a code like this, method with 2 overloads:

/**
 * Returns all keys of object that have specific value:
 * @example
 * KeysOfType<{a:1, b:2, c:1}, 1> == 'a' | 'c'
 */
type KeysOfType<MapT extends Record<string, any>, ValT> = {
    [K in keyof MapT]: MapT[K] extends ValT ? K : never;
}[keyof MapT];

export class Hooks<EventsMap extends Record<string, any>> {
    fireHooks<K extends KeysOfType<EventsMap, void>>(
        event: K,
        context: void,
    ): Promise<void>;

    fireHooks<K extends keyof EventsMap>(
        event: K,
        context: EventsMap[K],
    ): Promise<void>;

    fireHooks<K extends keyof EventsMap>(event: K, context: EventsMap[K]) {
        // ...
    }
}

It is supposed to be used like that:

type MyHooks = { 
  aaa: void; 
  bbb: { data: string } 
};
let h = new Hooks<MyHooks>();

h.fireHooks('aaa');
h.fireHooks('bbb', { data: 'data' });

There is a generic class Hooks with method fireHooks that takes events name-to-context-data map to validate context of event. Some of the events can be called w/o any context, so I want this method to call these events with only one argument.

Everything works fine, but the problem is that these overloads look ugly and overkill.

If I skip overloads, TS complains on h.fireHooks('aaa'); with

Expected 2 arguments, but got 1.ts(2554)

If I skip only 2nd overload, which seem to be 100% copy of implementation, TS complains on h.fireHooks('bbb', { data: 'data' }); with

Argument of type '"bbb"' is not assignable to parameter of type '"aaa"'.ts(2345)

Can someone explain

  • why do I need 1st overload to be able to skip parameter
  • and why I need 2nd overload, which seem to be redundant as it completely the same as implementation?
1 Answers

"why I need 2nd overload, which seem to be redundant as it completely the same as implementation?"

When you use overloads you can't actually call the implementation directly.

Observe:

function foo(a: string, b: string): void;
function foo(a: number, b: number): void;
function foo(a: number | string, b: number | string): void {
  //...
}

foo(1, 2) // good
foo('a', 'b') // good
foo(1, 'b') // Error: No overload matches this call.

Even though foo's implementation would allow one argument to be a number, and the other to be a string, the overloads don't allow that.

So you never have just one overload, and one implementation. Those would be identical.

You have either one function with one signature, or you have two or more overloads as well as one implementation.


"why do I need 1st overload to be able to skip parameter"

// You can also leave off the argument entirely
fireHooks<K extends KeysOfType<EventsMap, void>>(
    event: K,
): Promise<void>;

Because you have two function signatures, and you need an overload for each since (as stated above) you cannot directly call the implementation. Each have different type for event, since the operate on different keys.

Related