Search code examples
genericstypescriptvariadic

Function that takes a predicate function (returning a boolean), and returns a predicate function with the same parameters


So, I want to create a negate function, that takes some function that returns a boolean for some list of arguments, and returns a function that takes the same arguments and produces the exact opposite boolean result.

This is possible if we leave type-safety by the wayside:

function negate(predicate: Function): Function {
    return function () {
        return !predicate.apply(this, arguments);
    }
}

We can even indicate that the resulting function returns a boolean by using () => boolean as the return type. But this signature indicates that the returned function takes no arguments – in reality, it should get exactly the same arguments as the passed in predicate function.

What I would like is to be able to specify this. If I restrict predicate to exactly one parameter, I can:

function negate<A>(predicate: (a: A) => boolean): (a: A) => boolean {
    return function (a: A) {
        return !predicate.apply(this, arguments);
    }
}

And I could use overloading to specify this for zero, one, two, and so on parameters, by manually defining each version. At some point, I’ll be able to accept as many parameters as any function should reasonably be taking. And extending it with yet-more parameters, should my definition of “reasonable” expand, isn’t terribly much work.

Alternatively, this answer suggests a different tactic:

export function negate<Predicate extends Function>(p: Predicate): Predicate {
    return <any> function () {
        !p.apply(this, arguments);
    }
}

Which is type-safe so long as the Predicate actually does return a boolean but has no way of restricting the parameter to things that return boolean (and worse, this would silently cast whatever non-boolean result you get to a boolean in order to apply ! and this would not be indicated in the returned function’s signature).

So what I would really like to be both completely type-safe and DRY.

Does Typescript have any feature that would make this possible?


Solution

  • Figured out a way.

    interface IPredicate {
        (...args: any[]): boolean;
    }
    function negate<Predicate extends IPredicate>(p: Predicate): Predicate {
        return <any> function (): boolean {
            return !p.apply(this, arguments);
        }
    }
    

    The IPredicate interface allows us to define that whatever arguments we take, we expect a boolean result, and then using <Predicate extends IPredicate> allows us to define that the argument p and the returned function are to take the same parameters, since they must both be the same Predicate that extends IPredicate.

    Internal to the negate function, we’re using apply and so the type information is lost. Thus <any> is used to basically assure the TypeScript compiler that the interior function is correct. That makes the interior non-type-safe, but it is small and does not require maintenance once written.

    This can also be expanded to handle other logical functions on predicates, e.g. union:

    export function union<Predicate extends IPredicate>(
        p: Predicate,
        ...rest: Predicate[]
    ): Predicate {
        if (rest.length > 0) {
            return <any> function () {
                return p.apply(this, arguments) || union.apply(this, rest).apply(this, arguments);
            }
        }
        else {
            return p;
        }
    }