Okay so the title is weird. But it seems strange that you can't pass a function which takes a type which implements the interface the passed-in function is supposed to take:
interface Empty { }
class Implements implements Empty {
constructor(public s: string) {}
}
type TakesEmpty = (e: Empty) => void;
function TakesImplementor(e: Implements): void {}
function f(t: TakesEmpty) { }
/*
Argument of type '(e: Implements) => void' is not assignable to parameter of type 'TakesEmpty'.
Types of parameters 'e' and 'e' are incompatible. Property 's' is missing in type 'Empty' but required in type 'Implements'.
*/
f(TakesImplementor);
// But this works
function te (e: Empty): void {}
te(new Implements(""));
Why is this the case?
Well, in your example code nothing bad will actually happen at runtime because the functions don't do anything at all. But given the signature of TakesImplementor()
, it could be implemented like this:
function TakesImplementor(e: Implements): void {
console.log(e.s.toUpperCase());
}
That's fine, right? TakesImplementor()
accepts a parameter named e
of type Implements
, which has an s
property of type string
. Inside TakesImplementor()
you can access e.s.toUpperCase()
.
And then, given the signature of f()
, it could be implemented like this:
function f(t: TakesEmpty) {
t({});
}
That's also fine, right? The t
parameter passed into f()
is itself a function of the type TakesEmpty
, meaning you should be able to call t({})
, since the object {}
is a valid Empty
.
Yep, everything great until you go ahead and run this:
f(TakesImplementor);
And it explodes at runtime. f(TakesImplementor)
calls TakesImplementor({})
, which calls {}.s.toUpperCase()
, which doesn't exist because {}.s
is undefined. Boom. That is why the line f(TakesImplementor)
gives an error message:
// Property 's' is missing in type 'Empty' but required in type 'Implements'.
TakesImplementor
is not a TakesEmpty
, because TakesImplementor
only accepts Implements
types, while TakesEmpty
is required to accept all Empty
types, not just Implements
types.
It might be counterintuitive, but when dealing with functions, you narrow its type when you widen the type of its parameters, and you widen its type when you narrow the type of its parameters. This "going-the-other-way" of function parameters is known as contravariance. They vary in the opposite way, or "contra-vary". (This is as opposed to function return types, which widen the function type when widened and narrow the function type when narrowed... they vary together, or "co-vary", and thus are covariant.)
By narrowing the parameter type of TakesImplementor
from Empty
to Implements
, you have widened the type of TakesImplementor
itself compared to TakesEmpty
. Every TakesEmpty
can be used in place of TakesImplementor
, but not vice versa. Contravariant.
Contravariance of function parameter types is enforced in TypeScript 2.6 and above when you use the --strictFunctionTypes
compiler flag (also enabled with just --strict
). It's generally a good thing to enforce.
Hope that helps; good luck!
Collected from the Internet
Please contact [email protected] to delete if infringement.
Comments