fix(checker): suppress false TS7008 for empty array assigned to annotated expando property#3979
Conversation
…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>
There was a problem hiding this comment.
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. |
| propName := lhs.AsPropertyAccessExpression().Name().Text() | ||
| prop := c.getPropertyOfType(containerType, propName) | ||
| if prop == nil { | ||
| return nil | ||
| } | ||
| return c.getTypeOfSymbol(prop) |
There was a problem hiding this comment.
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.
|
@Zelys-DFKH Appreciate the effort, but I have a simpler fix in #3986. |
|
Thanks for the review, @ahejlsberg. I went the long way around, re-deriving through the AST what |
Fixes #3976
Credit to @chriskrycho, who traced the regression to
#3680and 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 whenfncarries an explicit type annotation that declares the property.The root cause is a circular type resolution path. When
getAssignmentDeclarationInitializerTypeevaluatesfn.items = [], it callscheckExpressionForMutableLocationon the[]RHS. That function tries to establish a contextual type by resolvingfn.items, which requires computing the type of the assignment declaration — which is the function we're currently in. The circularity causes[]to resolve asnever[]instead ofstring[]. The existingisEmptyArrayLiteralTypecheck then seesnever[]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 = []:getSymbolOfNode(binaryExpr)→itemssymbol (set by the binder on the assignment declaration).Parent→ the arrow function's symbol.ValueDeclaration→ the arrow function node.Parent(AST) → theVariableDeclarationcarrying the explicit type annotationtryGetTypeFromTypeNode(varDecl)→ reads the annotation type without triggering inferencegetPropertyOfType(containerType, "items")+getWriteTypeOfSymbol(prop)→ the write type of the property (for optional properties:string[], notstring[] | 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 internalmissingtype thatgetTypeOfSymbolreturns raw. Changed togetWriteTypeOfSymbol, which strips it viaremoveMissingType: the right behavior for any assignment-context type lookup. Added a compiler test with// @exactOptionalPropertyTypes: truecovering 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: