If I define a method inside an interface and then implement it later on, it's parameters types aren't being enforced if the Interface was using method signature (it works for property signature).
it looks a lot like a bug, is it? is it intentional? is there a way to tackle it without changing all of our functions to property signatures?
it's different than this question since in the question, the interface's method accepts some parameter and the implementation can work without it (which is logical). But here the implementation expects to receive an expanded parameter which doesn't exist in the Interface
It's intentional. A little background about function parameters and variance (which you can skip if you already know this):
If I ask for a function that accepts a
string
, and you give me a function that acceptsunknown
, I will be happy. I can safely pass that function astring
and it will accept it becausestring
is assignable tounknown
. This is contravariance, because the direction of assignability switches:string
is assignable tounknown
, therefore(x: unknown) => void
is assignable to(x: string) => void
.Compare this to covariance, where the direction of assignability stays the same:
"foo"
is assignable tostring
, therefore is(x: "foo") => void
assignable to(x: string) => void
? No, it's not. If I ask for a function that accepts astring
, and you give me a function that accepts only the string literal"foo"
, I will probably be unhappy if I use it. If I pass that function astring
that doesn't happen to be"foo"
, then the function will not accept it.So it's type safe to check function parameters contravariantly, and not type safe to check them covariantly. So what does TypeScript do? Well, before TypeScript 2.6, the compiler actually allowed both. That is, it checked all function and method parameters bivariantly.
Why would it do this? One of the issues has to do with methods that modify an object. Consider the example in the linked FAQ: the
push()
method of arrays. We usually want anstring[]
to be assignable tounknown[]
. If I only read from such an array, this is perfectly safe. I ask for anunknown[]
; you give me astring[]
; I read from it, pulling outunknown
values (which happen to bestring
objects but that's fine). But writing to the array is unsafe. If I callarray.push(123)
, that should be fine ifarray
isunknown[]
but very bad if it isstring[]
. If the parameter topush()
were checked contravariantly only, this would enforce the safety, disallowingstring[]
to be assignable tounknown[]
. But, as they say in the FAQ, that would be "incredibly annoying". Instead, they allow the unsafe-yet-convenient convention of allowing function and method parameters to vary both ways.Back to the answer to this question: Luckily for those of us who like a little more type safety, TypeScript 2.6 introduced the
--strictFunctionTypes
flag which, when enabled, causes function parameter types to be checked only contravariantly instead of bivariantly.But there's still that issue with
Array
and other built-in classes that are often used covariantly. To prevent--strictFunctionTypes
from being "incredibly annoying", the tradeoff is that function parameters are checked contravariantly, but method parameters are still checked bivariantly. So as of TS2.6, the compiler cares about the difference between method signatures and function signatures when it comes to type checking parameters.And so with the following interface:
You get the following behavior (in TS2.6+ with
--strictFunctionTypes
enabled):Note that in
contravariant
andcovariant
,func
is implemented as a method andmethod
is implemented as a function-valued property, but they are still checked according to the rules in theExample
interface:func
has a function signature and is checked strictly, andmethod
has a method signature and is checked loosely.So, is there a way to tackle it without changing all of your functions to property signatures? Probably nothing great. You might try to transform the signatures programmatically, like this:
which works in this case:
but there may be edge cases so I don't know if that's worth it to you.
Anyway, hope that helps; good luck!
Link to code