-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Skip overloads with too short function parameters #11905
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Skip overloads with too short function parameters #11905
Conversation
If the parameter of an overload is a function and the argument is also a function, skip the overload if the parameter has fewer arguments than the argument does. That overload cannot possibly apply, and should not participate in, for example, contextual typing. Example: ```ts interface I { (a: number): void; (b: string, c): void; } declare function f(i: I): void; f((x, y) => {}); ``` This code now skips the first overload instead of considering. This was a longstanding bug but was only uncovered now that more functions expressions are context sensitive.
1. Update changed baseline. 2. Add a new test with baseline.
@@ -11890,6 +11890,11 @@ namespace ts { | |||
// If the effective argument type is 'undefined', there is no synthetic type | |||
// for the argument. In that case, we should check the argument. | |||
if (argType === undefined) { | |||
// If the parameter and argument are both functions and the parameter has fewer arguments than the argument, | |||
// then this signature is not applicable. Exit early. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Exit early to avoid fixing incorrect contextual types to the function expression parameters"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@@ -11904,6 +11909,21 @@ namespace ts { | |||
return true; | |||
} | |||
|
|||
function isAritySmaller(sourceType: Type, target: Expression) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Explicit return false
at the bottom here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added
let targetParameterCount = 0; | ||
for (; targetParameterCount < target.parameters.length; targetParameterCount++) { | ||
const param = target.parameters[targetParameterCount]; | ||
if (param.initializer || param.questionToken || param.dotDotDotToken || isJSDocOptionalParameter(param)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be consistent, we should actually remove these checks. Optional parameters are considered to be present for the purposes of callback type checking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This skips some additional overloads. It is not bad but it is worse than what we do today. Here's the test failure:
function foo6<T>(cb: { (x: T): string; (x: T, y?: T): string }) {
return cb;
}
var r11 = foo6(<T>(x: T, y?: T) => ''); // any => string (+1 overload)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This gives r11: any
instead of r11: { (x: T): string; (x: T, y?: T): string }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made the change; it's easy enough to revert if we decide against it.
if (isFunctionExpressionOrArrowFunction(target) && isFunctionType(sourceType)) { | ||
const sourceSignatures = getSignaturesOfType(sourceType, SignatureKind.Call); | ||
const sourceLengths = sourceSignatures.map(sig => !sig.hasRestParameter ? sig.parameters.length : Number.MAX_VALUE); | ||
return forEach(sourceLengths, len => len < target.parameters.length); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A target with more parameters than the source might still be applicable if some of the target's parameters are optional. This doesn't appear to factor that in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous commit took optional parameters into account, but @RyanCavanaugh pointed out that optional parameters are considered to be present for checking function parameters. The change is in a single commit so it's easy to revert if you think that optionality should be taken into account, though.
I think a better approach would be to simply disqualify contextual signatures that have fewer parameters than the number of non-optional parameters in the function expression or arrow function. In other words, never contextually type from a signature that couldn't validly call the function. We already disqualify anything but a single non-generic signature, this could just be an additional check. I think it would solve the issue in #11875, but it would be a more local change with no effect on the overload resolution rules. |
Unfortunately, that results in var use: Overload;
use((req, res) => {});
interface Overload {
(handler1: (req1: string) => void): void;
(handler2: (req2: number, res2: number) => void): void;
} |
Yes, but that example didn't work before the change either, and It just never makes sense to contextually type part of a function expression's parameter list from a signature that couldn't actually call that function expression. Also, it is less of a breaking change to go from some specific type to The core issue here is that once we contextually type a parameter list, the assigned types are permanent. Changing that is much more involved and out of scope for this fix. Until then, I think what I'm proposing is the best we can do. |
OK, I moved the check to |
// If the contextual signature has fewer parameters than the function expression, do not use it | ||
if (isFunctionExpressionOrArrowFunction(node) && isAritySmaller(type, node)) { | ||
return undefined; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be the last check you do. That way you only have to deal with a single signature as everything else will have been disqualified.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it goes after the !Union check, then it only applies to unions of signatures. This is as late as it can go and still apply to non-union signatures.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved the check inside getNonGenericSignature so that it gets called in both the union and non-union paths. Let me know if that looks good to you.
@@ -10377,6 +10381,26 @@ namespace ts { | |||
return result; | |||
} | |||
|
|||
function isAritySmaller(sourceType: Type, target: FunctionExpression | ArrowFunction) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change this to operate on a single source signature and call it as the last check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. The call is inside getNonGenericSignature
now.
Fixes #11875
If the parameter of an overload is a function and the argument is also a
function, skip the overload if the parameter has fewer arguments than
the argument does. That overload cannot possibly apply, and should not
participate in, for example, contextual typing.
Example:
This code now skips the first overload instead of considering it.
This was a longstanding bug but was only uncovered now that more function expressions are context sensitive.
@mhegazy @RyanCavanaugh do you mind taking a look? I need to skip
checkExpressionWithContextualType
so I think the code has to be here, but it seems like it is operating at the wrong level of abstraction overall.