Sitelet https://github.com/microsoft/TypeScript/pull/63984
Skip to content

Fix TS2454 false positive for closed-over mutable variables - #63984

Queued
Mohit Nayak (mohit-nayak) wants to merge 3 commits into
microsoft:mainfrom
mohit-nayak:main
Queued

Fix TS2454 false positive for closed-over mutable variables#63984
Mohit Nayak (mohit-nayak) wants to merge 3 commits into
microsoft:mainfrom
mohit-nayak:main

Conversation

@mohit-nayak

@mohit-nayak Mohit Nayak (mohit-nayak) commented Aug 24, 2026

Copy link
Copy Markdown

Fixes #63981

Problem

Under --strict, a top-level let that is assigned at the top level gets reported as used before assignment when a closure compound-assigns it:

export {};

let count: number;

function makeCallback(p: string) {
  return () => {
    count += 1; // error TS2454: Variable 'count' is used before being assigned.
    return p;
  };
}

count = 0;
makeCallback('x')();

count is assigned before the callback can ever run, and the reference inside the arrow is an outer-variable reference, so definite assignment analysis should not fire here. 5.7 and 6.0 both accept this. The error is new in 7.

Cause

ensureAssignmentsMarked returned early whenever the symbol already had a non-zero lastAssignmentPos. That is not a safe "already computed" signal, because lastAssignmentPos can be written by a walk of a nested function that never visits the declaring container.

What happens in the repro:

  1. Checking "return p" inside the arrow calls isPastLastAssignment(p). p is declared in makeCallback, so markNodeAssignments walks only that function's subtree.
  2. That walk sees "count += 1". The referencing function (the arrow) is not the declaring function (the source file), so it records count.lastAssignmentPos = MaxInt32. It never reaches the top-level "count = 0", so hasDefiniteAssignment stays false.
  3. Checking "count += 1" then calls isSymbolAssignedDefinitely(count), which hits the early return. The source file is never walked, so "count = 0" is never seen.
  4. With hasDefiniteAssignment false, isNeverInitialized becomes true, which clears assumeInitialized even though isOuterVariable is true, and the error is reported.

Strada has no equivalent early return. Its only guard is the AssignmentsMarked node-links flag on the declaring container, which is per-container and therefore correct.

Solution

Drop the early return. The AssignmentsMarked flag together with hasParentWithAssignmentsMarked already guarantees each container is walked at most once, so the removed check only saved one FindAncestor and a map lookup, and it did that by skipping the walk that produces the right answer. This puts the behavior back in line with Strada.

Tests

Added tsc/testdata/tests/cases/compiler/definiteAssignmentOfClosedOverVariable.ts with three cases:

  • the repro from the issue, which must now be clean
  • the same bug without "return p", using a separate nested closure that references the parameter, which shows the trigger is the early nested walk and not the returned expression
  • a variable that really is never definitely assigned, which must still report TS2454

The baseline has exactly one diagnostic, on the third case. Running the same file through tsc@5.7.3 gives the identical single error on the identical line, so the port now matches Strada in both directions.

Across the full suite the only baselines written were the three for this new test, so nothing else moved. That is worth noting because ensureAssignmentsMarked also feeds isSymbolAssigned and isPastLastAssignment, which affect narrowing of closed-over variables and not just TS2454.

Checklist

Copilot AI balanced review requested due to automatic review settings August 24, 2026 17:48
@github-project-automation github-project-automation Bot moved this to Not started in PR Backlog Aug 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes a TS2454 false positive by ensuring assignment analysis runs for the variable’s declaring container.

Changes:

  • Removes an unsafe symbol-level early return.
  • Adds regression and negative test cases.
  • Records expected diagnostic, type, and symbol baselines.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
tsc/internal/checker/flow.go Corrects assignment-marking behavior.
tsc/testdata/tests/cases/compiler/definiteAssignmentOfClosedOverVariable.ts Adds regression coverage.
tsc/testdata/baselines/reference/compiler/definiteAssignmentOfClosedOverVariable.errors.txt Captures the remaining valid diagnostic.
tsc/testdata/baselines/reference/compiler/definiteAssignmentOfClosedOverVariable.types Records inferred types.
tsc/testdata/baselines/reference/compiler/definiteAssignmentOfClosedOverVariable.symbols Records symbol resolution.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

@mohit-nayak

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-project-automation github-project-automation Bot moved this from Not started to Needs merge in PR Backlog Aug 25, 2026
@ahejlsberg
Anders Hejlsberg (ahejlsberg) added this pull request to the merge queue Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Needs merge

Development

Successfully merging this pull request may close these issues.

TS2454 false positive when a callback mutates an outer let and returns the enclosing function's parameter

3 participants