Optional callback function parameters

Topics: General, Language Specification
Jan 4, 2013 at 5:58 AM

I was creating a .d.ts file for lodash, built on another one created for underscore.  And ran into an interesting issue, the underscore file been defined with overloads that appeared as follows.


    list: string[],
    iterator?: (element: string, index?: number, list?: string[]) => bool,
    context?: any): bool;


Now it defines index and list as optional parameters, but in this case the compiler expects them to be defined on the calling method, as optional parameters, when you try to use this method.

Now one of the nice things about underscore, is you can simply define a quick function, to filter your collection and continue on.  With this case in the compiler, you would have to define all 3 callback parameters, each time you wanted to use this method.  This isn't particularly helpful when you're looking to use the methods, in addition you're just adding extra bytes to your minified output.

I was able to get around this problem, fairly simply, by defining an overload for each optional parameter, as follows.

    list: string[],
    callback?: (element: string) => bool,
    context?: any): bool;
    list: string[],
    callback?: (element: string, index: number) => bool,
    context?: any): bool;
    list: string[],
    callback?: (element: string, index: number, list: string[]) => bool,
    context?: any): bool;

Now this is super tedious to do for each and every method, it gets worse if you have 4 different input types for list (string, string[], any[], number[], Object, any, etc, (generics will solve this partially once they can implemented)).

If there was a terser way to define this, and clearly show the consuming developer what potential methods the can implement it would be easier to create declaration files and instead of having 18 overloads you'd only have 6 or less.

    list: string[],
    callback?: (element: string [, index: number, list: string[]]) => bool,
    context?: any): bool;

Any thoughts?  Comments, suggestions?

Jan 8, 2013 at 10:48 PM

The short answer is that you shouldn't use optional arguments in the type of a callback. In other words, just declare the function like this:

    list: string[],
    callback?: (element: string, index: number, list: string[]) => bool,
    context?: any): bool;

You don't need overloads because implementations of the callback are always free to omit parameters, regardless of whether they're optional or not. For example:

any(myList, e => e == "hello");
any(myList, (s, x) => x < 10);

would both be acceptable calls to the any() function. As a rule, a function type is always assignable to another function type with the same or more parameters because it is always fine for a function to ignore parameters it isn't interested in.

Now, the interesting thing is that if you declare the index and list parameters to be optional then any callback that includes those parameters must also declare them optional. So, the second line above would need to be:

any(myList, (s, x?) => x < 10);

That's likely not what you intended and may seem surprising until you consider the meaning of the ? modifier from a callee's perspective. For a callee, the modifier means that you might not be given an argument for the particular parameter. Therefore, a function that requires an argument for a particular parameter (such as (x: number) => bool) can't be assigned to a function reference that, when called, might not supply an argument for that parameter (such as (x?: number) => bool).

The upshot of this is that callback parameters whose function types have optional parameters are actually MORE restrictive than callback parameters whose function types have non-optional parameters. When you declare

    list: string[],
    callback?: (element: string, index?: number, list?: string[]) => bool,
    context?: any): bool;

you are basically saying that the callback might or might not receive arguments for the index and list parameters. Therefore, handlers of the callback cannot require those parameters (by declaring them non-optional) but must be prepared for them to be missing (by declaring them optional).

So, in short, never use optional parameters in callbacks!

Jul 31, 2013 at 10:48 PM
Edited Jul 31, 2013 at 10:48 PM
How does a user know that arguments to the callback are optional if they are not explicitly listed as so? If the parameters are not marked with '?' then as a user I now have to check documentation or best guess as to how many parameters I need to provide -- or simply provide all because the declaration does not tell me. This to me seems like it is against what TypeScript is trying to achieve.

Sorry for reviving this dead thread but someone posted it on my underscore.d.ts implementation since it has this problem, and I'm not sure I completely agree with the answer here. -> https://github.com/jbaldwin/underscore.d.ts/issues/4

"you are basically saying that the callback might or might not receive arguments for the index and list parameters. " That is exactly the point of declaring them as '?' is it not? As a user I now know for a fact I can omit those parameters if I want to, but the compiler is unfortunately enforcing me to provide them in my callback declaration.

So why is it more restrictive? What was the design decision behind this?
Aug 1, 2013 at 4:24 AM
I agree, I think there needs to be a little bit of intellisense help to inform users don't have to provide a function that can handle all parameters.
Aug 1, 2013 at 2:15 PM
Edited Aug 1, 2013 at 2:17 PM
It seems inconsistent to me as well, for example:
class Foo {
    public bar(a?, b?, c?): void {...}

var foo = new Foo();
foo.bar(); // OK
foo.bar(1); // OK
foo.bar(1, 2); // OK
foo.bar(1, 2, 3); // OK
foo.bar(1, 2, 3, 4); // Error! but this is expected with the way the function is defined
Now consider:
class Foo2 {
    public bar2(fn: (a?, b?, c?) => void): void { ... }

var foo2 = new Foo2();
foo2.bar2(() => { ...}): // Error! but it seems like a OK call since a,b,c are optional
foo2.bar2(a) => { ...}); // error again... 
foo2.bar2((a, b) => {...}); // error
foo2.bar2((a?, b?, c?) => { ... });  // OK, but maybe I don't need b and c? I don't care about bar2's internals
This doesn't quite jive with what I would expect as a user.
Aug 1, 2013 at 3:53 PM
The parameters are optional from the caller's side, not the callee's side. If a callback parameter is optional from the caller's side, then the callee cannot depend on it being present (i.e. cannot pass in a function that requires a value).

Let's look at some examples
function forEach<T>(arr: T[], callback: (elem: T, index: number, arr: T[]) => void) {
    for(var i = 0; i < arr.length; i++) {
        callback(arr[i], i, arr);
Here, callback is defined without optional parameters because the function implementation always calls it with all its parameters. You want code like this to be legal:
var items = [ 1, 2, 3 ];
forEach(items, (el) => console.log(el));
Note that it is not forEach's job to decide which parameters you must accept. It wouldn't make sense to force you to declare a bunch of parameters you don't care about.

What would an actual optional callback look like? Let's imagine a forEach that does need its callback parameter to be optional:
function wackyForEach(arr: any[], callback: (elem: any, index: number, length?: number) => void) {
    for(var i = 0; i < arr.length; i++) {
        if(typeof arr[i] === 'string') {
            callback(arr[i], i, arr[i].length);
        } else {
            callback(arr[i], i);            

// Error: third parameter might not always be present
wackyForEach(items, (e, i, len) => console.log(len));

// OK
wackyForEach(items, (e, i, len?) => console.log(len || '(none/zero)'));

// OK
wackyForEach(items, e => console.log(e));
Here, the we cannot pass a function that requires more parameters than are guaranteed to be invoked by the caller.
Aug 1, 2013 at 4:11 PM
Edited Aug 1, 2013 at 4:50 PM
I see, this makes more sense. '?' is not denoting that you can omit it from the callback signature (what I thought it was doing), but rather it is saying this is a required parameter but it may or may not have a value so you must declare it in the callback signature and potentially handle both cases. Thanks for the in-depth response, this was helpful.

edit: This seems really obvious in retrospect, I guess the clue is recognizing who the caller and callee are, and that a regular function it must handle the optional parameters, so naturally the same would apply to a callback if it is defined like that...