Search code examples
angulartypescriptcapacitor

How to infer arguments types of a multiple signature method in TypeScript?


I use various Capacitor plugins in my Angular app.

They basically consists on methods, and also listeners. Here comes an example from Keyboard plugin :

addListener(eventName: 'keyboardWillShow', listenerFunc: (info: KeyboardInfo) => void): Promise<PluginListenerHandle> & PluginListenerHandle;

addListener(eventName: 'keyboardDidShow', listenerFunc: (info: KeyboardInfo) => void): Promise<PluginListenerHandle> & PluginListenerHandle;

addListener(eventName: 'keyboardWillHide', listenerFunc: () => void): Promise<PluginListenerHandle> & PluginListenerHandle;

addListener(eventName: 'keyboardDidHide', listenerFunc: () => void): Promise<PluginListenerHandle> & PluginListenerHandle;

I created a method that calls these addListener methods, but I don't know how to type it to achieve what I want :

myMethod(Keyboard, 'keyboardWillShow') // return type : Observable<KeyboardInfo>
myMethod(Keyboard, 'keyboardWillHide') // return type : Observable<void>
myMethod(Keyboard, 'nonexisting') // TS Error

I want to type my method in a way that it accepts plugin in first argument, and only accepts in second arg event names for which an addListener overload exists and accept it as its first arg. I also want, if possible, to infer the type of the callback argument according to the event name.

For now, I have no idea of how to achieve this.

type CapacitorPlugin = {
  addListener: (eventName: any, listener: (event: any) => void) => Promise<PluginListenerHandle> & PluginListenerHandle
}

...

export function fromCapacitorListener(plugin: CapacitorPlugin, eventName: unknown): Observable<any> { /* ... */ }

Solution

  • Here is the typescript example demonstrating, multiple return types based on an input parameter.

    First we define the events in a type.

    type TypeName = "keyboardWillShow" | "keyboardDidShow" | "keyboardWillHide" | "keyboardDidHide";
    

    Then we define interface that contains the return types. Then we can use T extends "keyboardWillShow" to conditionally set the return types.

    type ObjectType<T extends TypeName> =
        T extends "keyboardWillShow" ? Observable<KeyboardInfo> :
        T extends "keyboardDidShow" ? Observable<void> :
        T extends "keyboardWillHide" ? Observable<string> :
        T extends "keyboardDidHide" ? Observable<number> :
        never;
    

    We can use Generics of Typescript to obtain the type of the parameter, then feed it to ObjectType, which will give use the generic object types.

    function myMethod<T extends TypeName>(model: Keyboard, parameter: T): ObjectType<T> {
        return '';
    }
    const keyboard = new Keyboard();
    const test1 = myMethod(keyboard, "keyboardWillShow");
    const test2 = myMethod(keyboard, "keyboardDidShow");
    const test3 = myMethod(keyboard, "keyboardWillHide");
    const test4 = myMethod(keyboard, "keyboardDidHide");
    

    Full Code:

    export class Keyboard {
    
    }
    
    
    type TypeName = "keyboardWillShow" | "keyboardDidShow" | "keyboardWillHide" | "keyboardDidHide";
    
    export interface KeyboardInfo { }
    
    export interface Observable<T> {
        test: T;
    }
    
    type ObjectType<T extends TypeName> =
        T extends "keyboardWillShow" ? Observable<KeyboardInfo> :
        T extends "keyboardDidShow" ? Observable<void> :
        T extends "keyboardWillHide" ? Observable<string> :
        T extends "keyboardDidHide" ? Observable<number> :
        never;
    
    function myMethod<T extends TypeName>(model: Keyboard, parameter: T): ObjectType<T> {
        return '';
    }
    const keyboard = new Keyboard();
    const test1 = myMethod(keyboard, "keyboardWillShow");
    const test2 = myMethod(keyboard, "keyboardDidShow");
    const test3 = myMethod(keyboard, "keyboardWillHide");
    const test4 = myMethod(keyboard, "keyboardDidHide");
    

    Typescript Playground Demo