Skip to content

fix(checker): suppress false TS7008 for empty array assigned to annotated expando property#3979

Closed
Zelys-DFKH wants to merge 4 commits into
microsoft:mainfrom
Zelys-DFKH:fix/callable-type-empty-array-ts7008
Closed

fix(checker): suppress false TS7008 for empty array assigned to annotated expando property#3979
Zelys-DFKH wants to merge 4 commits into
microsoft:mainfrom
Zelys-DFKH:fix/callable-type-empty-array-ts7008

Conversation

@Zelys-DFKH
Copy link
Copy Markdown

@Zelys-DFKH Zelys-DFKH commented May 18, 2026

Fixes #3976

Credit to @chriskrycho, who traced the regression to #3680 and the exact nightly in the bug report. That diagnosis was the map.

Analysis

Callable-type variables with expando property assignments like fn.items = [] were incorrectly producing TS7008 ("Member 'items' implicitly has an 'any[]' type") even when fn carries an explicit type annotation that declares the property.

The root cause is a circular type resolution path. When getAssignmentDeclarationInitializerType evaluates fn.items = [], it calls checkExpressionForMutableLocation on the [] RHS. That function tries to establish a contextual type by resolving fn.items, which requires computing the type of the assignment declaration — which is the function we're currently in. The circularity causes [] to resolve as never[] instead of string[]. The existing isEmptyArrayLiteralType check then sees never[] and fires TS7008.

For plain object variables (const obj: { tags?: string[] } = {}), this doesn't occur because the binder doesn't create an assignment declaration symbol for that case (plain objects aren't expando initializers in TS mode).

Fix

Added getPropertyTypeFromContainerAnnotation, a helper called before the TS7008 diagnostic fires. It navigates through the AST to the explicit type annotation on the container variable — bypassing the circular resolution path entirely — and looks up the declared property type. If an annotation covers the property, that type is returned directly and TS7008 is skipped.

The navigation path for const fn: T = () => undefined; fn.items = []:

  1. getSymbolOfNode(binaryExpr)items symbol (set by the binder on the assignment declaration)
  2. .Parent → the arrow function's symbol
  3. .ValueDeclaration → the arrow function node
  4. .Parent (AST) → the VariableDeclaration carrying the explicit type annotation
  5. tryGetTypeFromTypeNode(varDecl) → reads the annotation type without triggering inference
  6. getPropertyOfType(containerType, "items") + getWriteTypeOfSymbol(prop) → the write type of the property (for optional properties: string[], not string[] | missing; for accessor properties: the setter type)

If any step returns nil (no annotation, wrong declaration shape, etc.), the helper returns nil and TS7008 fires as before. This preserves the diagnostic for genuinely untyped cases like const fn3 = () => undefined; fn3.labels = [].

Second commit: Under exactOptionalPropertyTypes, optional properties carry an internal missing type that getTypeOfSymbol returns raw. Changed to getWriteTypeOfSymbol, which strips it via removeMissingType: the right behavior for any assignment-context type lookup. Added a compiler test with // @exactOptionalPropertyTypes: true covering both annotated containers (no error) and the unannotated control (TS7008 still fires).

Regression introduced by #3680.

Copilot Checklist

I successfully ran these commands at the end of my session, and they completed without error:

  • npx hereby build
  • npx hereby test
  • npx hereby lint
  • npx hereby format

Zelys-DFKH and others added 3 commits May 18, 2026 16:32
…ated property

When a property access assignment like `fn.items = []` appears on a variable
with an explicit type annotation, the empty array literal resolves to `never[]`
due to circular type resolution — causing TS7008 even though the declared type
provides full context.

Fix by consulting the container's explicit type annotation before reporting the
implicit-any diagnostic. If the annotation names the property with a concrete
type, return that type directly and skip the diagnostic.

Preserves the existing TS7008 behavior for genuinely untyped cases (e.g.,
`this.bar = []` in a JS method with no declared type on the container).

Fixes microsoft#3976.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rrors)

Verifies that fn3.labels = [] without a type annotation on the container
still produces TS7008, confirming the fix only bypasses the diagnostic
when an explicit type annotation covers the property.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 18, 2026 22:19
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 checker regression where expando assignments like fn.items = [] on callable-typed variables incorrectly triggered TS7008 even when the container variable has an explicit type annotation that declares the property.

Changes:

  • Updates assignment-declaration initializer typing to suppress TS7008 for empty-array RHS when the container’s explicit annotation provides a declared property type.
  • Adds a checker helper to retrieve the property type directly from the container variable’s type annotation to avoid circular type resolution.
  • Adds a compiler test case and reference baselines covering callable-type and object-type annotated containers, plus an unannotated control that should still error.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
internal/checker/checker.go Adds annotation-based property type lookup to bypass circular resolution and avoid false TS7008 on empty-array expando assignments.
testdata/tests/cases/compiler/callableTypePropertyEmptyArrayAssignment.ts New regression test for TS7008 suppression with annotated callable/object containers and a failing unannotated case.
testdata/baselines/reference/compiler/callableTypePropertyEmptyArrayAssignment.types Reference types baseline for the new test.
testdata/baselines/reference/compiler/callableTypePropertyEmptyArrayAssignment.symbols Reference symbols baseline for the new test.
testdata/baselines/reference/compiler/callableTypePropertyEmptyArrayAssignment.errors.txt Reference errors baseline ensuring TS7008 still occurs for the unannotated case.

Comment thread internal/checker/checker.go Outdated
Comment on lines +18034 to +18039
propName := lhs.AsPropertyAccessExpression().Name().Text()
prop := c.getPropertyOfType(containerType, propName)
if prop == nil {
return nil
}
return c.getTypeOfSymbol(prop)
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch. This is an assignment context, and getWriteTypeOfSymbol is the canonical choice here. It calls removeMissingType for optional properties, which strips the internal missing type under exactOptionalPropertyTypes. Downstream assignability checking then sees string[] instead of string[] | missing. It also uses the setter type for accessor properties, which is semantically correct when writing. Added a compiler test with // @exactOptionalPropertyTypes: true to explicitly cover this path.

…actOptionalPropertyTypes

Under exactOptionalPropertyTypes, optional properties are modeled
internally as T | missing. getTypeOfSymbol returns that raw type;
getWriteTypeOfSymbol strips the missing constituent via removeMissingType,
which is the correct behavior for an assignment-context type lookup.

Also adds a compiler test that exercises this path with
exactOptionalPropertyTypes: true, covering both annotated containers
(no error) and an unannotated control case (TS7008 still fires).

Addresses review comment from Copilot on PR microsoft#3979.
@ahejlsberg
Copy link
Copy Markdown
Member

@Zelys-DFKH Appreciate the effort, but I have a simpler fix in #3986.

@Zelys-DFKH
Copy link
Copy Markdown
Author

Thanks for the review, @ahejlsberg. I went the long way around, re-deriving through the AST what symbol.Parent already captures, which is exactly why mine needed the PropertyAccessExpression guard and yours didn't. Good lesson in starting from the right layer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tsgo does not correctly infer array type assignment on callable type with fields since 20260503.1

4 participants