Reputation: 7025
This is probably a continuation of this question. I have an enum
export enum RewardAdPluginEvents {
Loaded= 'onRewardedVideoAdLoaded',
FailedToLoad= 'onRewardedVideoAdFailedToLoad',
Showed= 'onRewardedVideoAdShowed',
FailedToShow= 'onRewardedVideoAdFailedToShow',
Dismissed= 'onRewardedVideoAdDismissed',
Rewarded= 'onRewardedVideoAdReward',
}
An I have an interface that needs overloads like this:
addListener(
eventName: RewardAdPluginEvents.Dismissed,
listenerFunc: () => void,
): PluginListenerHandle;
addListener(
eventName: RewardAdPluginEvents.FailedToLoad,
listenerFunc: (error: ErrorInfo) => void,
): PluginListenerHandle;
addListener(
eventName: RewardAdPluginEvents.Rewarded,
listenerFunc: (item: RewardItem) => void,
) : PluginListenerHandle;
Is there a way to force the interface to implement all that overloads?
I tried with
import type { PluginListenerHandle } from '@capacitor/core';
type Enum<E> = Record<keyof E, string> & { [k: string]: string };
export type HasListeners<Enum> = { addListener: (eventName: keyof Enum, listenerFunc: (info: any) => void) => PluginListenerHandle }
and then
export interface RewardDefinitions extends HasListeners<RewardAdPluginEvents>{
but it fails because it takes too many keys that come from an object:
Interface 'RewardDefinitions' incorrectly extends interface 'HasListeners'. Types of property 'addListener' are incompatible. Type '{ (eventName: RewardAdPluginEvents.Dismissed, listenerFunc: () => void): PluginListenerHandle; (eventName: RewardAdPluginEvents.FailedToLoad, listenerFunc: () => void): PluginListenerHandle; (eventName: RewardAdPluginEvents.FailedToShow, listenerFunc: () => void): PluginListenerHandle; (eventName: RewardAdPluginEven...' is not assignable to type '(eventName: number | "toString" | "charAt" | "charCodeAt" | "concat" | "indexOf" | "lastIndexOf" | "localeCompare" | "match" | "replace" | "search" | "slice" | "split" | "substring" | "toLowerCase" | ... 31 more ... | "trimEnd", listenerFunc: (info: any) => void) => PluginListenerHandle'. Types of parameters 'eventName' and 'eventName' are incompatible. Type 'number | "toString" | "charAt" | "charCodeAt" | "concat" | "indexOf" | "lastIndexOf" | "localeCompare" | "match" | "replace" | "search" | "slice" | "split" | "substring" | "toLowerCase" | ... 31 more ... | "trimEnd"' is not assignable to type 'RewardAdPluginEvents.Dismissed'. Type 'number' is not assignable to type 'RewardAdPluginEvents.Dismissed'
Upvotes: 1
Views: 473
Reputation: 15126
It's possible, but not terribly straightforward. Overloads are intersections of functions, which are trickier to manipulate than unions.
First you'll need something to turn a union into an intersection, which can be done with inference on a contravariant parameter in a conditional type (docs). This is a bit techical, so I'll omit the full explanation here.
type Contra<T> = (x: T) => void
type UnwrapContra<T> = [T] extends [Contra<infer S>] ? S : never
type UnionToIntersection<U> = UnwrapContra<U extends any ? Contra<U> : never>
For example, UnionToIntersection<{a:1}|{b:2}>
evaluates to {a:1} & {b:2}
. Now you'll need to get a union of all overloads, which is a bit more straightforward. Just declare a type Overload
that creates a single overload, and use a conditional type to distribute it over each member of the enum.
type Overload<T> =
(eventName: T, listenerFunc: (...args: any[]) => any) => PluginListenerHandle
type OverloadUnionForEnum<T> = T extends any ? Overload<T> : never
type OverloadUnion = OverloadUnionForEnum<RewardAdPluginEvents>
The resulting OverloadUnion
a union of overloads, one for each member of RewardAdPluginEvents
: Overload<RewardAdPluginEvents.FailedToLoad> | Overload<RewardAdPluginEvents.Dismissed> | ..
Turning this union into an intersection yields the actual overloaded function signature for addListener
:
type Overloads = UnionToIntersection<OverloadUnion>
With all that out of the way, you can now define a type Validate
that constrains its argument to have an addListener
property that conforms to the overloads:
type Validate<T extends {addListener: Overloads}> = T
To use Validate
, consider two interfaces ListenerA
and ListenerB
that both contain some of the overloads, and restrict RewardAdPluginEvents
to just Dismissed
, FailedToLoad
, and Rewarded
for now to keep the types a bit smaller:
interface ListenerA {
addListener(
eventName: RewardAdPluginEvents.Dismissed,
listenerFunc: () => void,
): PluginListenerHandle;
}
interface ListenerB {
addListener(
eventName: RewardAdPluginEvents.FailedToLoad,
listenerFunc: (error: ErrorInfo) => void,
): PluginListenerHandle;
addListener(
eventName: RewardAdPluginEvents.Rewarded,
listenerFunc: (item: RewardItem) => void,
) : PluginListenerHandle;
}
Validating either ListenerA
or ListenerB
fails as it should, with a large error message that has a somewhat readable last line:
type Invalid1 = Validate<ListenerA>
// ERROR: ... Type 'RewardAdPluginEvents.FailedToLoad' is not assignable to
// type 'RewardAdPluginEvents.Dismissed'.
type Invalid2 = Validate<ListenerB>
// ERROR: ... Type 'RewardAdPluginEvents.Dismissed' is not assignable to
// type 'RewardAdPluginEvents.FailedToLoad'.
When the two are combine, all overloads are present and, validation passes:
export type Combined = Validate<ListenerA & ListenerB> // No error!
Upvotes: 1