'Where a function is required, typescript allows me to pass an object with an incompatible `apply` property

Currently using typescript 3.4.5 with strict mode enabled...

Backstory

I just ran into a situation where typescript failed to protect me from my own mistakes, unfortunately. And I'm trying to figure out why typescript failed to catch this error.

I was writing a type declaration for a function like this:

function acceptVisitors (visitor) {
    visitor.apply(value);
}

Astute observers may point out that visitor's type could be defined in one of two ways — as a function, or as an object with an apply property:

type visitorType = (this: IValue) => void;
// or
type visitorType = {
    apply: (value: IValue) => void;
};

It turns out, in my case it was the latter. After adding the type declaration, I proceeded to write this incorrect code:

// This is incorrect because it doesn't pass it as an argument.
// Rather, the `this` context is set to the value.
acceptVisitors((value: IValue) => { ... });

Now, the puzzling thing is that Typescript did not show an error when I passed a function whose type was incompatible with visitorType.

Simplified example

Let's change the parameter type to a string, and walk through it.
I'm defining a type called func that is a function that requires a string argument.

type func = (param1: string) => void;

Functions by nature are callable objects that also have an apply method.

declare let f: func;
f.apply(undefined, ['str']);
// all good

Now here's the other type — an object with an apply property.

type objectWithApplyProp = {
    apply: (param1: string) => void;
};

We can call the apply property, but not in the same way...

declare let o: objectWithApplyProp;
o.apply(undefined, ['str']); // Error: Expected 1 arguments, but got 2.

And objectWithApplyProp has a call signature that doesn't work with func:

o.apply('str'); // ok
f.apply('str'); // Error: The 'this' context of type 'func' is not assignable to 
                // method's 'this' of type '(this: string) => void'

And further tests show that f is assignable to o, but not the other way around, which makes sense... all functions are objects but not all objects are callable.

But why is f considered assignable to o? The type of objectWithApplyProp requires an apply value that matches a certain type, and func doesn't match it

A function's apply signature should be inferrable from its parameters, but typescript doesn't seem to be inferring it.

So, any feedback is welcome. Am I wrong, or is there a limitation in Typescript? Is it a known issue? Thanks



Solution 1:[1]

So this is a technical reason of why it's happening, and a workaround:

Typescript's built-in lib/es5.d.ts declaration file defines Function.apply with parameters of type any. Also it defines Function.prototype as any.

interface Function {
    apply(this: Function, thisArg: any, argArray?: any): any;
    call(this: Function, thisArg: any, ...argArray: any[]): any;
    bind(this: Function, thisArg: any, ...argArray: any[]): any;
    toString(): string;
    prototype: any;
    readonly length: number;
    // Non-standard extensions
    arguments: any;
    caller: Function;
}

And I guess all function expressions are given the Function type by default.

So the function was allowed to be assigned to the object with the incompatible apply property because the function did not have a strongly typed apply method, based on the built-in Function types. Therefore typescript could not determine that the apply signatures were different.

Typescript 3.2 introduces CallableFunction which has generic arguments on its apply declaration. But I haven't figured out how to make it fix this problem.

A workaround is to define a stronger function type and manually assign it to the function. The workaround is a bit tedious, but it works.

interface func extends Function {
    (param1: string): void;
    // manually define `apply
    apply<T, R> (thisArg: T, args: [string]): R;
}
interface objectWithApplyProp { // unchanged
    apply: (param1: string) => void;
}
// Now we have proper errors here:
o = f;
// Type 'func' is not assignable to type 'objectWithApplyProp'.
//  Types of property 'apply' are incompatible.
//    Type '<T, R>(thisArg: T, args: [string]) => R' is not assignable to type '(param1: string) => void'. ts(2322)
f = o;
// Type 'objectWithApplyProp' is missing the following properties from type 'func': call, bind, prototype, length, and 2 more. ts(2740)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1