'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 |