Improve inference by not considering thisless functions to be context-sensitive#62243
Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR improves TypeScript's type inference by making functions that don't reference this not context-sensitive. Previously, all object literal methods were considered context-sensitive, causing poor inference when type parameters depended on inferring from these methods first. The change helps TypeScript better infer types in common patterns involving object literals with functions.
Key Changes:
- Modified
hasContextSensitiveParametersto check for actualthisusage rather than assuming all function-like declarations are context-sensitive - Updated binder to track
thiskeyword usage withNodeFlags.ContainsThisflag - Extended context-sensitive checking to include yield expressions in generators
Reviewed Changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/compiler/binder.ts | Adds tracking of this keyword usage and sets ContainsThis flag on functions |
| src/compiler/checker.ts | Updates context-sensitive checking to include yield expressions and generators |
| src/compiler/utilities.ts | Modifies hasContextSensitiveParameters and forEachYieldExpression to support new logic |
| tests/cases/compiler/*.ts | New test cases demonstrating improved inference for thisless functions |
| tests/baselines/reference/. | Updated baselines showing improved type inference results |
| (node as FunctionLikeDeclaration | ClassStaticBlockDeclaration).endFlowNode = currentFlow; | ||
| } | ||
| if (seenThisKeyword) { | ||
| node.flags |= NodeFlags.ContainsThis; |
There was a problem hiding this comment.
There's trailing whitespace at the end of this line. Remove the extra spaces.
| node.flags |= NodeFlags.ContainsThis; | |
| node.flags |= NodeFlags.ContainsThis; |
| case SyntaxKind.JSDocFunctionType: | ||
| case SyntaxKind.FunctionType: | ||
| case SyntaxKind.ConstructSignature: | ||
| case SyntaxKind.ConstructorType: |
There was a problem hiding this comment.
Type-level AST nodes had ContainerFlags.IsControlFlowContainer here. This was messing up some of the changes I made since it was interfering with the implemented seenThisKeyword tracking. I don't see why those would be considered control flow containers and there are no tests proving it was needed.
Other changes in this function are basically of the same kind - I just removed ContainerFlags.IsControlFlowContainer from the type-level nodes.
There was a problem hiding this comment.
We seem to have added at least one of those on purpose:
#8941
So I'm wondering why it's not needed anymore. @ahejlsberg do you remember why this was needed?
There was a problem hiding this comment.
As to SyntaxKind.PropertyDeclaration - given the test from the referenced PR still works just OK, I'd assume that its needs are covered by arrow functions being treated as flow containers (arrows were used as property declaration initializers in that test).
|
|
||
| function isContextSensitiveFunctionLikeDeclaration(node: FunctionLikeDeclaration): boolean { | ||
| return hasContextSensitiveParameters(node) || hasContextSensitiveReturnExpression(node); | ||
| return hasContextSensitiveParameters(node) || hasContextSensitiveReturnExpression(node) || !!(getFunctionFlags(node) & FunctionFlags.Generator && node.body && forEachYieldExpression(node.body as Block, isContextSensitive)); |
There was a problem hiding this comment.
Previously generators were always context-sensitive as they can't even be arrow functions. At times, they are truly context-sensitive in cases like:
declare function test(
gen: () => Generator<(arg: number) => string, void, void>,
): void;
test(function* () {
yield (arg) => String(arg);
});So I had to add this extra forEachYieldExpression to cover for this
There was a problem hiding this comment.
Just in terms of readability, maybe this would be better as its own function, then it can have a descriptive name like hasContextSensitiveYieldExpression and that example can be its documentation.
| // in that traversal terminates in the event that 'visitor' supplies a truthy value. | ||
| /** @internal */ | ||
| export function forEachYieldExpression(body: Block, visitor: (expr: YieldExpression) => void): void { | ||
| export function forEachYieldExpression<T>(body: Block, visitor: (expr: YieldExpression) => T): T | undefined { |
There was a problem hiding this comment.
this basically changes this function to work in the same way as forEachReturnStatement defined above
| const myStoreConnect: Connect = function( | ||
| >myStoreConnect : Connect | ||
| > : ^^^^^^^ | ||
| >function( mapStateToProps?: any, mapDispatchToProps?: any, mergeProps?: any, options: unknown = {},) { return connect( mapStateToProps, mapDispatchToProps, mergeProps, options, );} : <TStateProps, TOwnProps>(mapStateToProps?: any, mapDispatchToProps?: any, mergeProps?: any, options?: unknown) => InferableComponentEnhancerWithProps<TStateProps, Omit<P, Extract<keyof TStateProps, keyof P>> & TOwnProps> |
There was a problem hiding this comment.
this just changes the inferred type to match the type inferred from the equivalent arrow function with type parameters, it's purely a result of making a thisless function context-insensitive
| > : ^^^^^^^^^^^^^^^ | ||
| >strategy("Nothing", function* (state: State) { yield ; return state; // `return`/`TReturn` isn't supported by `strategy`, so this should error.}) : (a: State) => IterableIterator<State, void> | ||
| > : ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| >strategy("Nothing", function* (state: State) { yield ; return state; // `return`/`TReturn` isn't supported by `strategy`, so this should error.}) : (a: any) => IterableIterator<any, void> |
There was a problem hiding this comment.
similarly here, this is a result of inferSignatureInstantiationForOverloadFailure no longer skipping the generator function on the basis it's context-sensitive (inferSignatureInstantiationForOverloadFailure uses CheckMode.SkipContextSensitive)
| }, | ||
| } | ||
| impl.explicitVoid1 = function () { return 12; }; | ||
| >impl.explicitVoid1 = function () { return 12; } : (this: void) => number |
There was a problem hiding this comment.
those this parameters were not used by the assigned implementation - they were just auto-assigned to it based on the this parameter in the contextual signature
|
@typescript-bot test it |
|
Hey @jakebailey, I've packed this into an installable tgz. You can install it for testing by referencing it in your and then running There is also a playground for this build and an npm module you can use via |
|
Hey @jakebailey, the results of running the DT tests are ready. There were interesting changes: Branch only errors:Package: jqrangeslider |
|
@jakebailey Here are the results of running the user tests with tsc comparing There were infrastructure failures potentially unrelated to your change:
Otherwise... Something interesting changed - please have a look. Details
|
|
@jakebailey Here they are:
tscComparison Report - baseline..pr
System info unknown
Hosts
Scenarios
Developer Information: |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@jakebailey Here are the results of running the top 400 repos with tsc comparing Something interesting changed - please have a look. Details
|
|
…dates-based returns `any`/`unknown`
|
@typescript-bot test it |
|
Hey @jakebailey, I've packed this into an installable tgz. You can install it for testing by referencing it in your and then running There is also a playground for this build and an npm module you can use via |
|
Hey @jakebailey, the results of running the DT tests are ready. There were interesting changes: Branch only errors:Package: jqrangeslider |
|
@jakebailey Here are the results of running the top 400 repos with tsc comparing Something interesting changed - please have a look. Details
|
|
The perf run shows 5 new errors in vscode; I am not sure why they are not showing up in the top or user tests... |
|
@jakebailey thanks for noticing, I'll look into it |
|
microsoft/vscode#282223 should address VS Code break. A general fix for this issue could be a performant version of #57421 . I have prototyped locally a version of |
This error looks correct but is unfortunate because it's caused by the type cast ( |
implements @RyanCavanaugh's suggestion from #47599 :
fixes #62204
fixes #60986
fixes #58630
fixes #57572
fixes #56067
fixes #55489
fixes #55124
fixes #53924
fixes #50258