Reputation: 23201
I'm using TSC Version 2.4.2
Note this interface:
interface CallbackWithNameParameter {
cb: (name: string) => void
}
This implementation:
const aCallback: CallbackWithNameParameter = {
cb: () => {}
};
does not throw a compile error as you might expect.
Note that this does throw an error:
const aSecondCallback: CallbackWithNameParameter = {
cb: (num: number) => {}
};
Is this a TSC bug or am I misunderstanding something? Shouldn't aCallback
cause a compile error?
Upvotes: 1
Views: 912
Reputation: 328057
It's not a bug. A function with fewer trailing parameters is substitutable for a function with more trailing parameters (as long as the parameter types match). It's apparently such a common question that it is explicitly mentioned in the TypeScript FAQ.
The reasoning is roughly: any function in JavaScript may be called with any number of parameters, and it's always safe to have a caller send your function more parameters than you expect; you just ignore them. In your case,
const aCallback: CallbackWithNameParameter = {
cb: () => {}
};
is valid because aCallback.cb
ignores the first parameter, which must be a string
:
aCallback.cb('hey'); // no problem
The second case, aSecondCallback
, is invalid because it would incorrectly try to interpret the first parameter as a number
instead of the string
it will actually be. That's not safe and likely an error.
That's pretty much it. Hope that helps; good luck!
@jbmilgrom said
Thanks for your answer. It made total sense on first read, but now running into this:
aCallback.cb()
is a compile error, even though its definition is not. How does this fit in?
It is a compile error beause you declared that the type of aCallback
is CallbackWithNameParameter
, whose cb
property is a function requiring a string
parameter. TypeScript no longer knows or cares about the value of aCallback
, whose cb
property happens not to need a parameter.
If you had done this instead:
interface CallbackWithNoParameter {
cb: () => void
}
const aCallback: CallbackWithNoParameter = {
cb: () => {}
};
aCallback.cb(); // okay
it would work.
Substitutability is not generally symmetric:
declare let noParam: CallbackWithNoParameter;
declare let oneParam: CallbackWithNameParameter;
oneParam = noParam; // yes
noParam = oneParam; // no
You can assign a narrow type (a callback with no parameters) to a wider type (a callback with one parameter) without error, but you cannot do the reverse in general.
@jbmilgrom said
Thanks for Update 1! But why would typescript allow the definition of something that can never be called as defined? In this case,
aCallback
may successfully be defined as an empty function with no parameter when implementingCallbackWithNameParameter
. Yet, it cannot be called as such, since ya know, it is of typeCallbackWithNameParameter
. In other words (pertaining to symmetry of substitutability), why can you assign a narrow type to a wider type even though the ultimate reference (e.g.oneParam
) can never so used after assigned?
It's because you made TypeScript forget by declaring the wider type. You poured milk (narrow type, like CallbackWithNoParameter
) into a container marked "liquid" (wider type, like CallbackWithNameParameter
), and then tried to open the container and pour the liquid into your tea without checking that it wasn't, say, bleach. You may know that there's milk in the container, but TypeScript only knows that it's a liquid and tries to protect you. The best way to avoid this is to pour the milk into a container marked "milk". If someone asks you for liquid later you can give them the contents of a milk container, but if someone asks you for milk you can't give them the contents of a liquid container unless there's some way to check if it's milk first.
It isn't easy to check for whether a function accepts zero arguments without calling it at runtime and seeing if it blows up:
function isCallbackWithNoParameter(x: any): x is CallbackWithNoParameter {
if (!('cb' in x)) return false;
try {
x.cb()
} catch (e) {
return false;
}
return true;
}
declare let noParam: CallbackWithNoParameter;
declare let oneParam: CallbackWithNameParameter;
oneParam = noParam; // yes
noParam = oneParam; // no
oneParam.cb(); // no
if (isCallbackWithNoParameter(oneParam)) {
noParam = oneParam; // okay, you checked first
oneParam.cb(); // okay, you checked first
}
Upvotes: 2