Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
const saveExceptionTarget = currentExceptionTarget;
const saveActiveLabelList = activeLabelList;
const saveHasExplicitReturn = hasExplicitReturn;
const saveSeenThisKeyword = seenThisKeyword;
const isImmediatelyInvoked = (
containerFlags & ContainerFlags.IsFunctionExpression &&
!hasSyntacticModifier(node, ModifierFlags.Async) &&
Expand All @@ -1022,19 +1023,22 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
currentContinueTarget = undefined;
activeLabelList = undefined;
hasExplicitReturn = false;
seenThisKeyword = false;
bindChildren(node);
// Reset all reachability check related flags on node (for incremental scenarios)
node.flags &= ~NodeFlags.ReachabilityAndEmitFlags;
// Reset flags (for incremental scenarios)
node.flags &= ~(NodeFlags.ReachabilityAndEmitFlags | NodeFlags.ContainsThis);
Comment thread
gabritto marked this conversation as resolved.
if (!(currentFlow.flags & FlowFlags.Unreachable) && containerFlags & ContainerFlags.IsFunctionLike && nodeIsPresent((node as FunctionLikeDeclaration | ClassStaticBlockDeclaration).body)) {
node.flags |= NodeFlags.HasImplicitReturn;
if (hasExplicitReturn) node.flags |= NodeFlags.HasExplicitReturn;
(node as FunctionLikeDeclaration | ClassStaticBlockDeclaration).endFlowNode = currentFlow;
}
if (seenThisKeyword) {
node.flags |= NodeFlags.ContainsThis;
}
if (node.kind === SyntaxKind.SourceFile) {
node.flags |= emitFlags;
(node as SourceFile).endFlowNode = currentFlow;
}

if (currentReturnTarget) {
addAntecedent(currentReturnTarget, currentFlow);
currentFlow = finishFlowLabel(currentReturnTarget);
Expand All @@ -1051,12 +1055,15 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
currentExceptionTarget = saveExceptionTarget;
activeLabelList = saveActiveLabelList;
hasExplicitReturn = saveHasExplicitReturn;
seenThisKeyword = node.kind === SyntaxKind.ArrowFunction ? saveSeenThisKeyword || seenThisKeyword : saveSeenThisKeyword;
}
else if (containerFlags & ContainerFlags.IsInterface) {
const saveSeenThisKeyword = seenThisKeyword;
seenThisKeyword = false;
bindChildren(node);
Debug.assertNotNode(node, isIdentifier); // ContainsThis cannot overlap with HasExtendedUnicodeEscape on Identifier
node.flags = seenThisKeyword ? node.flags | NodeFlags.ContainsThis : node.flags & ~NodeFlags.ContainsThis;
seenThisKeyword = saveSeenThisKeyword;
}
else {
bindChildren(node);
Expand Down Expand Up @@ -2852,6 +2859,9 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
}
// falls through
case SyntaxKind.ThisKeyword:
if (node.kind === SyntaxKind.ThisKeyword) {
seenThisKeyword = true;
}
// TODO: Why use `isExpression` here? both Identifier and ThisKeyword are expressions.
if (currentFlow && (isExpression(node) || parent.kind === SyntaxKind.ShorthandPropertyAssignment)) {
(node as Identifier | ThisExpression).flowNode = currentFlow;
Expand Down Expand Up @@ -3833,28 +3843,27 @@ export function getContainerFlags(node: Node): ContainerFlags {
// falls through
case SyntaxKind.Constructor:
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.ClassStaticBlockDeclaration:
return ContainerFlags.IsContainer | ContainerFlags.IsControlFlowContainer | ContainerFlags.HasLocals | ContainerFlags.IsFunctionLike;
case SyntaxKind.MethodSignature:
case SyntaxKind.CallSignature:
case SyntaxKind.JSDocSignature:
case SyntaxKind.JSDocFunctionType:
case SyntaxKind.FunctionType:
case SyntaxKind.ConstructSignature:
case SyntaxKind.ConstructorType:
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

@gabritto gabritto Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

case SyntaxKind.ClassStaticBlockDeclaration:
return ContainerFlags.IsContainer | ContainerFlags.IsControlFlowContainer | ContainerFlags.HasLocals | ContainerFlags.IsFunctionLike;
return ContainerFlags.IsContainer | ContainerFlags.HasLocals | ContainerFlags.IsFunctionLike;

case SyntaxKind.JSDocImportTag:
// treat as a container to prevent using an enclosing effective host, ensuring import bindings are scoped correctly
return ContainerFlags.IsContainer | ContainerFlags.IsControlFlowContainer | ContainerFlags.HasLocals;
return ContainerFlags.IsContainer | ContainerFlags.HasLocals;

case SyntaxKind.FunctionExpression:
case SyntaxKind.ArrowFunction:
return ContainerFlags.IsContainer | ContainerFlags.IsControlFlowContainer | ContainerFlags.HasLocals | ContainerFlags.IsFunctionLike | ContainerFlags.IsFunctionExpression;

case SyntaxKind.ModuleBlock:
return ContainerFlags.IsControlFlowContainer;
case SyntaxKind.PropertyDeclaration:
return (node as PropertyDeclaration).initializer ? ContainerFlags.IsControlFlowContainer : 0;

case SyntaxKind.CatchClause:
case SyntaxKind.ForStatement:
Expand Down
12 changes: 8 additions & 4 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21143,9 +21143,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const { initializer } = node as JsxAttribute;
return !!initializer && isContextSensitive(initializer);
}
case SyntaxKind.JsxExpression: {
case SyntaxKind.JsxExpression:
case SyntaxKind.YieldExpression: {
// It is possible to that node.expression is undefined (e.g <div x={} />)
const { expression } = node as JsxExpression;
const { expression } = node as JsxExpression | YieldExpression;
return !!expression && isContextSensitive(expression);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

}
}
Expand All @@ -21154,7 +21155,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

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));
}

function hasContextSensitiveReturnExpression(node: FunctionLikeDeclaration) {
Expand Down Expand Up @@ -32629,7 +32630,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (inferenceContext && contextFlags! & ContextFlags.Signature && some(inferenceContext.inferences, hasInferenceCandidatesOrDefault)) {
// For contextual signatures we incorporate all inferences made so far, e.g. from return
// types as well as arguments to the left in a function call.
return instantiateInstantiableTypes(contextualType, inferenceContext.nonFixingMapper);
const type = instantiateInstantiableTypes(contextualType, inferenceContext.nonFixingMapper);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this?

Copy link
Copy Markdown
Contributor Author

@Andarist Andarist Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This addresses an effect break caught here. You can take a deeper look at the added test tests/cases/compiler/returnTypeInferenceContextualParameterTypesInGenerator1.ts. Without this the test runs into:

    export const layerServerHandlers = Rpcs.toLayer(
      gen(function* () {
        return {
          Register: (id) => String(id),
                     ~~
!!! error TS7006: Parameter 'id' implicitly has an 'any' type.
        };
      })
    );

In a way, this is a targeted fix that might address a fair chunk of real world scenarios. The problem with instantiateContextualType is, unfortunately, that its "lossy" at times (at least that can happen when return mappers are involved) - it can lose meaningful information for some nodes deeper in the tree. any/unknown don't provide any useful contextual information so this helps by keeping the contextual type in its uninstantiated form.

If you don't feel this approach is right, I'd have to dig into this again to refresh my memory on it to provide a better explanation.

Copy link
Copy Markdown
Contributor Author

@Andarist Andarist Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so I refreshed myself a little bit on this. As I mentioned, instantiateContextualType is tricky. I could probably easily find at least 5 issues related to it. Conceptually, the best contextual type might be one of the following:

  • uninstantiated contextual type (its constraints might end up being useful)
  • instantiated contextual type using inferred types and/or default
  • instantiated contextual type using return mapper

Only one of those survives instantiateContextualType though and at the time it gets called it's not exactly which one might be best (usually for nodes deeper in the AST). So given the current implementation, it's guaranteed there might be cases when this loses some useful information and it already happens today.

The above fix is targeted at the mentioned effect-related break but it should benefit more than that. Given the changes in this PR, a returnOnlyType was created: () => Generator<never, { Register: anyFunctionType; }, any>. That type is only created when hasContextSensitiveParameters(node) and this PR doesn't consider thisless generators to automatically have a context sensitive parameter. It's worth noting that returnOnlyType itself has ObjectFlags.NonInferrableType but its parts dont have that so inferences can be made from them. In fact, that's kinda the goal - as per the code comment:

// Skip parameters, return signature with return type that retains noncontextual parts so inferences can still be drawn in an early stage

So an early inference is made from never into Eff and that makes the first branch in instantiateContextualType to kick in. But the instantiated type isn't all that useful - it's just unknown - and a better candidate can be discovered based on the returnMapper: HandlersFrom<Rpc<"Register", number, string>>.

So this PR just ignores those simple intrinsic types here as they won't benefit any contextual typing anyway (tbf, we could also ignore never). This allows the logic to try the other source of instantiation - the returnMapper. And, in there, it discovers the more interesting/useful type (HandlersFrom<Rpc<"Register", number, string>>). In fact, this branch could also ignore any/unknown and let the type to trickle down to the uninstantiated T. I pushed this out in ec8f425

if (!(type.flags & TypeFlags.AnyOrUnknown)) {
return type;
}
}
if (inferenceContext?.returnMapper) {
// For other purposes (e.g. determining whether to produce literal types) we only
Expand Down
24 changes: 14 additions & 10 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2880,19 +2880,24 @@ export function forEachReturnStatement<T>(body: Block | Statement, visitor: (stm
}
}

// Warning: This has the same semantics as the forEach family of functions,
// 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 {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this basically changes this function to work in the same way as forEachReturnStatement defined above

return traverse(body);

function traverse(node: Node): void {
function traverse(node: Node): T | undefined {
switch (node.kind) {
case SyntaxKind.YieldExpression:
visitor(node as YieldExpression);
const value = visitor(node as YieldExpression);
if (value) {
return value;
}
const operand = (node as YieldExpression).expression;
if (operand) {
traverse(operand);
if (!operand) {
return;
}
return;
return traverse(operand);
case SyntaxKind.EnumDeclaration:
case SyntaxKind.InterfaceDeclaration:
case SyntaxKind.ModuleDeclaration:
Expand All @@ -2905,14 +2910,13 @@ export function forEachYieldExpression(body: Block, visitor: (expr: YieldExpress
if (node.name && node.name.kind === SyntaxKind.ComputedPropertyName) {
// Note that we will not include methods/accessors of a class because they would require
// first descending into the class. This is by design.
traverse(node.name.expression);
return;
return traverse(node.name.expression);
}
Comment thread
gabritto marked this conversation as resolved.
}
else if (!isPartOfTypeNode(node)) {
// This is the general case, which should include mostly expressions and statements.
// Also includes NodeArrays.
forEachChild(node, traverse);
return forEachChild(node, traverse);
}
}
}
Expand Down Expand Up @@ -10829,7 +10833,7 @@ export function hasContextSensitiveParameters(node: FunctionLikeDeclaration): bo
// an implicit 'this' parameter which is subject to contextual typing.
const parameter = firstOrUndefined(node.parameters);
if (!(parameter && parameterIsThisKeyword(parameter))) {
return true;
return !!(node.flags & NodeFlags.ContainsThis);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
//// [tests/cases/compiler/circularlySimplifyingConditionalTypesNoCrash.ts] ////

=== Performance Stats ===
Instantiation count: 1,000

=== circularlySimplifyingConditionalTypesNoCrash.ts ===
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
>Omit : Omit<T, K>
Expand Down Expand Up @@ -71,8 +68,8 @@ declare var connect: Connect;
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>
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

> : ^ ^^ ^^ ^^^ ^^ ^^^ ^^ ^^^ ^^ ^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>function( mapStateToProps?: any, mapDispatchToProps?: any, mergeProps?: any, options: unknown = {},) { return connect( mapStateToProps, mapDispatchToProps, mergeProps, options, );} : (mapStateToProps?: any, mapDispatchToProps?: any, mergeProps?: any, options?: unknown) => InferableComponentEnhancerWithProps<TStateProps, Omit<P, Extract<keyof TStateProps, keyof P>> & TOwnProps>
> : ^ ^^^ ^^ ^^^ ^^ ^^^ ^^ ^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

mapStateToProps?: any,
>mapStateToProps : any
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
first.js(23,9): error TS2554: Expected 1 arguments, but got 0.
first.js(31,5): error TS2416: Property 'load' in type 'Sql' is not assignable to the same property in base type 'Wagon'.
Type '(files: string[], format: "csv" | "json" | "xmlolololol") => void' is not assignable to type '(supplies?: any[]) => void'.
Type '(files: string[], format: "csv" | "json" | "xmlolololol") => void' is not assignable to type '(supplies?: any[] | undefined) => void'.
Target signature provides too few arguments. Expected 2 or more, but got 1.
first.js(47,24): error TS2507: Type '(numberEaten: number) => void' is not a constructor function type.
generic.js(19,19): error TS2554: Expected 1 arguments, but got 0.
generic.js(20,32): error TS2345: Argument of type 'number' is not assignable to parameter of type '{ claim: "ignorant" | "malicious"; }'.
second.ts(8,25): error TS2507: Type '(numberEaten: number) => void' is not a constructor function type.
second.ts(14,7): error TS2417: Class static side 'typeof Conestoga' incorrectly extends base class static side 'typeof Wagon'.
Types of property 'circle' are incompatible.
Type '(others: (typeof Wagon)[]) => number' is not assignable to type '(wagons?: Wagon[]) => number'.
Type '(others: (typeof Wagon)[]) => number' is not assignable to type '(wagons?: Wagon[] | undefined) => number'.
Types of parameters 'others' and 'wagons' are incompatible.
Type 'Wagon[]' is not assignable to type '(typeof Wagon)[]'.
Property 'circle' is missing in type 'Wagon' but required in type 'typeof Wagon'.
Expand Down Expand Up @@ -52,7 +52,7 @@ second.ts(17,15): error TS2345: Argument of type 'string' is not assignable to p
load(files, format) {
~~~~
!!! error TS2416: Property 'load' in type 'Sql' is not assignable to the same property in base type 'Wagon'.
!!! error TS2416: Type '(files: string[], format: "csv" | "json" | "xmlolololol") => void' is not assignable to type '(supplies?: any[]) => void'.
!!! error TS2416: Type '(files: string[], format: "csv" | "json" | "xmlolololol") => void' is not assignable to type '(supplies?: any[] | undefined) => void'.
!!! error TS2416: Target signature provides too few arguments. Expected 2 or more, but got 1.
if (format === "xmlolololol") {
throw new Error("please do not use XML. It was a joke.");
Expand Down Expand Up @@ -95,7 +95,7 @@ second.ts(17,15): error TS2345: Argument of type 'string' is not assignable to p
~~~~~~~~~
!!! error TS2417: Class static side 'typeof Conestoga' incorrectly extends base class static side 'typeof Wagon'.
!!! error TS2417: Types of property 'circle' are incompatible.
!!! error TS2417: Type '(others: (typeof Wagon)[]) => number' is not assignable to type '(wagons?: Wagon[]) => number'.
!!! error TS2417: Type '(others: (typeof Wagon)[]) => number' is not assignable to type '(wagons?: Wagon[] | undefined) => number'.
!!! error TS2417: Types of parameters 'others' and 'wagons' are incompatible.
!!! error TS2417: Type 'Wagon[]' is not assignable to type '(typeof Wagon)[]'.
!!! error TS2417: Property 'circle' is missing in type 'Wagon' but required in type 'typeof Wagon'.
Expand Down
4 changes: 2 additions & 2 deletions tests/baselines/reference/generatorTypeCheck62.types
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ export const Nothing2: Strategy<State> = strategy("Nothing", function*(state: St
export const Nothing3: Strategy<State> = strategy("Nothing", function* (state: State) {
>Nothing3 : Strategy<State>
> : ^^^^^^^^^^^^^^^
>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>
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

> : ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>strategy : <T extends StrategicState>(stratName: string, gen: (a: T) => IterableIterator<T | undefined, void>) => (a: T) => IterableIterator<T | undefined, void>
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^^^^
>"Nothing" : "Nothing"
Expand Down
Loading