Search code examples
typescripttypescript-types

Define a Typescript type for a function with none or many different parameter types


I have a function which binds a passed function to a HTMLElement:

  global.d.ts:
  type tPrimitivRestParams = (number | object | string)[];
  type tDefaultVoidFunction = (...params: tPrimitivRestParams) => void;
  export default class AwesomeFunctions {
    public static addClickCallback(id: HTMLElement, func: tDefaultVoidFunction, scope: object, ...argArray: tPrimitivRestParams): void {
      [...] // checks
      // eslint-disable-next-line no-null/no-null
      element.onclick = func ? func.bind(scope, ...argArray) : null;
    }
  }
  export default class AwesomeClass {
    private bindMe(id: string):void {}
    private bindMeToo():void {}
    private bindMeThree(a:string, b: number, c: object): void {}
    private bindMeFour(clazz: SomeClass): void {}

    private binder():void {
      AwesomeFunctions.addClickCallback(document.getElementById('...'), this.bindMeToo, this); // TS says yes
      AwesomeFunctions.addClickCallback(document.getElementById('...'), this.bindMe, this, 'param'); // TS says no
      AwesomeFunctions.addClickCallback(document.getElementById('...'), this.bindMeThree, this, 'param', 2, {whoopi: 'goldberg', age: 24, 'actual-age': 'dontcha dare askn!!!'}); // TS says no
      AwesomeFunctions.addClickCallback(document.getElementById('...'), this.bindMeThree, this, new SomeClass()); // TS says no
    }
  }

It's working fine with JS but TS yells for all but the first:

// Example for bindMe
S2345: Argument of type '(id: string) => void' is not assignable to parameter of type 'tDefaultVoidFunction'.

If I do this:

AwesomeFunctions.addClickCallback(..., this.bindMe as tDefaultVoidFunction , ...);

TS is satisfied, but it seems wrong to me.

I know it's a problem with the tDefaultVoidFunction type. Is it possible to define a type to make TS happy with all the bindings? I can't use any, null or Function.

Sidequestion:

I also could do this:

AwesomeFunctions.addClickCallback(..., () => this.bindMe('param'), this);

TS is also satisfied but 95% of the addClickCallback function gets pointless. But that's not the reason for my hesitation of using an arrow function. I fear the scopes could differ. As far as I can tell, the scope is always a this. So in this case I claim the arrow function and the bind act the same. Am I wrong?


Reproducible example:

type tPrimitivRestParams = (number | object | string)[];
type tDefaultVoidFunction = (...params: tPrimitivRestParams) => void;

class AwesomeFunctions {
    public static addClickCallback(id: string, func: tDefaultVoidFunction, scope: object, ...argArray: tPrimitivRestParams): void {
        // DO something
    }
}

class AwesomeClass {
    private bindMe(id: string):void {}
    private bindMeToo():void {}
    private bindMeThree(a:string, b: number, c: object): void {}

    private binder():void {
        AwesomeFunctions.addClickCallback('A', this.bindMeToo, this);
        AwesomeFunctions.addClickCallback('B', this.bindMe, this, 'param');
        AwesomeFunctions.addClickCallback('C', this.bindMeThree, this, 'param', 2, {whoopi: 'goldberg', age: 24, 'actual-age': 'dontcha dare askn!!!'});
    }
}

Playground: TS Example


Solution

  • That's what you tell the compiler:

    type tDefaultVoidFunction = (...params: (number | object | string)[]) => void;
    

    The callback must accept any number of parameters where each parameter must accept number | object | string.

    Let's have a look at your function...

    const example: tDefaultVoidFunction = (id: string): void {}
    

    The parameter accepts only strings and so the compiler will tell you that's wrong. (number and object missing)

    You can define multiple allowed signatures by using union types:

    type tDefaultVoidFunction = ((id: string) => void) 
        | (() => void) 
        | ((a: string, b: number, c: object) => void) 
        | ((clazz: SomeClass) => void);
    

    Notes:

    Update based on comments

    Possible option:

    Instead of writing:

    AwesomeFunctions.addClickCallback(document.getElementById('...'), this.bindMeToo, this);
    

    use:

    document.getElementById('...')?.addEventListener('click', (e) => this.bindMeTo());
    // ignoring that we do not provide valid parameters in both cases