Skip to content

Preserve error elaboration for indexed access type assignments#3220

Open
WhiteMinds wants to merge 1 commit into
microsoft:mainfrom
WhiteMinds:fix/improve-indexed-access-error-elaboration
Open

Preserve error elaboration for indexed access type assignments#3220
WhiteMinds wants to merge 1 commit into
microsoft:mainfrom
WhiteMinds:fix/improve-indexed-access-error-elaboration

Conversation

@WhiteMinds

Copy link
Copy Markdown

Fixes microsoft/TypeScript#63206

Note: I understand the current contribution scope is limited to 6.0/7.0 differences and crashes. This is a diagnostic quality improvement rather than a behavioral change — no type-checking logic is altered, only the error message detail level. Happy to hold this until 7.0 if preferred.

Problem

When assigning to an indexed access type (e.g. this["faa"]), reportRelationError unconditionally clears errorChain before reporting the generic "could be instantiated with an arbitrary type" message, discarding the specific elaboration already computed from the constraint comparison (missing properties, type mismatches, etc.).

Fix

Preserve errorChain when the target is an IndexedAccess type, so users see both the generic message and the specific details.

Before

this.foo = {}
// Type '{}' is not assignable to type 'this["faa"]'.
//   'this["faa"]' could be instantiated with an arbitrary type which could be unrelated to '{}'.

After

this.foo = {}
// Type '{}' is not assignable to type 'this["faa"]'.
//   'this["faa"]' could be instantiated with an arbitrary type which could be unrelated to '{}'.
//     Property 'key' is missing in type '{}' but required in type '{ key: number | null; }'.

this.foo = {key: "str"}
// Type '{ key: string; }' is not assignable to type 'this["faa"]'.
//   'this["faa"]' could be instantiated with an arbitrary type which could be unrelated to '{ key: string; }'.
//     Type '{ key: string; }' is not assignable to type '{ key: number | null; }'.
//       Types of property 'key' are incompatible.
//         Type 'string' is not assignable to type 'number'.

Two existing baselines updated to reflect the additional elaboration (no errors removed, only more specific detail added).

@jakebailey

Copy link
Copy Markdown
Member

Maybe I don't have context here, but, I don't find the new messages more helpful than the existing one...

@gary-donut

Copy link
Copy Markdown

Maybe I don't have context here, but, I don't find the new messages more helpful than the existing one...

Consider this example:

class A {
    declare foo: this["faa"];
    declare faa: {key: number|null}

    fuu() {
        this.foo = {}           // vague error
        this.foo = {key: "str"} // vague error
    }
}

let foo: {key: number|null};
foo = {}           // precise error
foo = {key: "str"} // precise error

Both assign to the same underlying type {key: number|null}.
Direct assignment gives clear errors — "Property 'key' is missing"* or *"Type 'string' is not assignable to type 'number'".
But via this["faa"], you only get "could be instantiated with an arbitrary type which could be unrelated to '{}'".

Comment on lines +4718 to 4721
if target.flags&TypeFlagsIndexedAccess == 0 {
r.errorChain = nil
}
r.reportError(diagnostics.X_0_could_be_instantiated_with_an_arbitrary_type_which_could_be_unrelated_to_1, targetType, generalizedSourceType)

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.

Looking at the error in general, it looks pretty confusing to have this message anywhere other than at the bottom of an elaboration.

Maybe guard all of this in an if r.errorChain != nil

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.

Do you mean skipping the entire "could be instantiated..." elaboration when errorChain already has specific diagnostics? Something like adding r.errorChain == nil to the outer guard:

if targetFlags&TypeFlagsTypeParameter != 0 && target != r.c.markerSuperTypeForCheck && target != r.c.markerSubTypeForCheck && r.errorChain == nil {

So when the constraint comparison has already produced precise errors (e.g., missing property or type mismatch), we skip the generic message entirely instead of prepending it.

Before (current):

Type '{}' is not assignable to type 'this["faa"]'.
  'this["faa"]' could be instantiated with an arbitrary type which could be unrelated to '{}'.

After:

Type '{}' is not assignable to type 'this["faa"]'.
  Property 'key' is missing in type '{}' but required in type '{ key: number | null; }'.
Type '{ key: string; }' is not assignable to type 'this["faa"]'.
  Type '{ key: string; }' is not assignable to type '{ key: number | null; }'.
    Types of property 'key' are incompatible.
      Type 'string' is not assignable to type 'number'.

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.

No, I was thinking the opposite (r.errorChain != nil, not r.errorChain == nil). The idea is to only override the chain with the specialized error if there was a chain to begin with, but only drop the rest of the chain if the target type isn't an indexed access type.

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.

Thanks for the clarification. Implemented as suggested: guarded the whole block with r.errorChain != nil, and only clearing the chain for non-indexed-access targets in the default branch.

if targetFlags&TypeFlagsTypeParameter != 0 && ... && r.errorChain != nil {
    switch {
    case ...:
        r.reportError(different_subtype...)
    case ...:
        r.reportError(different_subtype...)
    default:
        if target.flags&TypeFlagsIndexedAccess == 0 {
            r.errorChain = nil
        }
        r.reportError(arbitrary_type...)
    }
}

This affects ~196 baselines since the generic message is now skipped when there's no prior error chain.
Net effect is ~600 fewer lines across baselines, removing cases where "could be instantiated" appeared alone without specific context.

@WhiteMinds
WhiteMinds force-pushed the fix/improve-indexed-access-error-elaboration branch 2 times, most recently from 1c9829a to 9c6d671 Compare March 31, 2026 13:01
When assigning to an indexed access type like this["faa"], the checker
resolves the constraint and performs a structural comparison that
generates specific error messages (e.g., missing properties or type
mismatches). However, reportRelationError unconditionally clears
errorChain before reporting the generic "could be instantiated with an
arbitrary type" message, discarding the useful elaboration.

Only add the specialized type parameter elaboration when there's already
an error chain from the constraint comparison. When the target is an
indexed access type, preserve the existing chain so users see the
specific details; otherwise, replace it with the generic message.

When no error chain exists, skip the elaboration entirely — the generic
message is confusing when it appears alone without specific context.

Fixes microsoft/TypeScript#63206
@WhiteMinds
WhiteMinds force-pushed the fix/improve-indexed-access-error-elaboration branch from 9c6d671 to f2c2423 Compare March 31, 2026 13:10
@RyanCavanaugh RyanCavanaugh added this to the Possible Improvement milestone Apr 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

5 participants