1

I want to pass different callback functions as an argument and have them called with proper parameters.

Here is a highly simplified example of how should it work. Except that process instanceof ITwo has no sense and I cannot find any expression that does the job.

    interface IOne { (j: string): number; }
    interface ITwo { (j: number): number; }
    function dualArg(i: string, process: IOne | ITwo): number {
        // something like this
        if (process instanceof ITwo) {
            // call with numeric arg
            return process(i);
        } else {
            // conver to number
            return process(+i);
        }
    }
    function inc(i: number): number {
        return ++i;
    }
    function idem(text: string): number {
        return +text;
    }
    it('determine function signature', () => {
        expect(dualArg('1', inc)).toBe(2);
        expect(dualArg('1', idem)).toBe(1);
    })

For a normal argument instanceof will be enough for TypeScript to treat it as an object of specific type, however there does not seem to be anything similar for functions.

If I use some kind of hard-coded conditional, such as process.prototype.constructor.name === 'idem' I get Typescript error message: Cannot invoke an expression whose type lacks a call signature. Type 'IOne | ITwo' has no compatible call signatures.

Here of course I could define process: any to disable any TypeScript checks and the code will compile and run, but my goal is to be able to distinguish the functions just by their signature (and not rely on some other convention such as name or additional flags).

Normunds Kalnberzins
  • 1,213
  • 10
  • 20
  • does this help? https://stackoverflow.com/questions/46703364/why-does-instanceof-in-typescript-give-me-the-error-foo-only-refers-to-a-ty – Ric Feb 21 '18 at 13:33
  • not really, that one is about telling difference between normal parameters/object classes. That problem has a couple of clear solutions. Though yes, using interface to distinguish things fails in the same way. – Normunds Kalnberzins Feb 21 '18 at 21:23

2 Answers2

3

The problem is that at runtime all type information is lost. So you can't directly reason about the type of a function at runtime (beyond the fact that it is a function).

What you could do is create a function type that also has a property that determines the type. And use a function to build-up the function:

enum Type { One, Two}
interface IOne { (j: string): number; type: Type.One }
interface ITwo { (j: number): number; type: Type.Two}
function dualArg(i: string, process: IOne | ITwo): number {
    if (process.type === Type.One) {
        // Type guard above, process is of type IOne
        return process(i);
    } else {
        // Type guard above, process is of type ITwo
        return process(+i);
    }
}
function inc(i: number): number {
    return ++i;
}
function idem(text: string): number {
    return +text;
}

function oneFunction(fn: (j: string)=> number) : IOne{
    return Object.assign(fn, { type: Type.One as Type.One });
}

function twoFunction(fn: (j: number)=> number) : ITwo{
    return Object.assign(fn, { type: Type.Two as Type.Two });
}

dualArg("", twoFunction(inc));
dualArg("", oneFunction(idem));

For your simple example this would be overkill (you could just define two versions of dualArg) but if the creation of the function and the usage are far apart and there is more code that is reused between the two this approach might make sense.

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • I think this will not work the same way as my ...constructor.name check. Unless I define process: any. TypeScript will protest about signature incompatibility. – Normunds Kalnberzins Feb 21 '18 at 21:07
  • @NormundsKalnberzins, no I tested before I posted, it works, type inference and all. Try it, if something does not work as expected let me know. – Titian Cernicova-Dragomir Feb 21 '18 at 21:27
  • 1
    wow, in that case this is practically the answer. Not really out-of-box that I hoped for, but pretty close – Normunds Kalnberzins Feb 21 '18 at 21:39
  • can we write the same more compact? Say it works inline as dualArg('1', Object.assign(idem, { type: Type.One as Type.One })); or dualArg('1', Object.assign(idem, { type: 0 })); and even dualArg('1', Object.assign((text: string): number {return +text;}, { type: 0 })); Bu can we completely get rid of Object.assign()? – Normunds Kalnberzins Feb 22 '18 at 10:08
  • Yon assign it manually, but it won't really be shorter. Maybe renae the functions to something shorter, and do the assignement on declaration not on usage: `let inc = asTwo((i: number): number =>{ return ++i; });` – Titian Cernicova-Dragomir Feb 22 '18 at 10:14
0

TypeScript is a dev time only. You can't expect it to do something in run-time. TypeScript is able to "understand" things using your run-time assumptions (like instanceof).

It looks like your two functions should have one implementation with two overloads, and not deal with it in the dualArg (calling) function. Dealing with the parameter types inside dualArg means that you will have to do the same thing everywhere you want to call these functions.

So how about you implement a wrapper function that will do the parameter tests (In run-time) and TypeScript will detect that, and protect you.

gilamran
  • 7,090
  • 4
  • 31
  • 50
  • I do not mean I cannot resolve the coding problem. All I need to do is make all callback functions use the same signature and make dualArgs provide all possible parameters - that's what I did, but - ugly, right? Or use wrapper as you propose. My dualArgs does always same heavy lifting and is complex and callback functions are light, but different for different situations. My frustration is about being able to tell that my callback may be of different types and not being able to use this :-) – Normunds Kalnberzins Feb 21 '18 at 21:18