Skip to content

Excess-property check fires on a nested object literal that is later assigned to an annotated return type, where tsc 6.0 does not #3491

@tomquist

Description

@tomquist

Steps to reproduce

tsconfig.json:

{
  "compilerOptions": {
    "strict": true,
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "noEmit": true
  }
}

index.ts:

interface TypeData {
  ref?: string;
}

interface UsageData {
  Type_Data?: Array<TypeData>;
  Comments?: string;
}

interface EmailAddressData {
  Email_Address: string;
  Usage_Data?: Array<UsageData>;
}

declare function condition(): boolean;
declare function makeRef(s: string): string;
declare const emails: { value: string; isWork: boolean }[];

async function build(): Promise<EmailAddressData[]> {
  let emailAddressData;
  if (condition()) {
    const primary = emails[0];
    emailAddressData = primary
      ? [
          {
            Email_Address: primary.value,
            Usage_Data: [
              {
                attributes: { "wd:Public": false },
                Type_Data: [
                  {
                    attributes: { "wd:Primary": true },
                    ref: makeRef("HOME"),
                  },
                ],
              },
            ],
          },
        ]
      : [];
  } else {
    emailAddressData = emails.map((email) => ({
      Email_Address: email.value,
      Usage_Data: [
        {
          attributes: { "wd:Public": email.isWork },
          Type_Data: [
            {
              attributes: { "wd:Primary": true },
              ref: makeRef("HOME"),
            },
          ],
        },
      ],
    }));
  }
  return emailAddressData;
}
build().then(console.log);

Tested with @typescript/native-preview@beta (Version 7.0.0-dev.20260421.2) and typescript@6.0.3.

The attributes field doesn't exist on UsageData or TypeData — but neither does it exist in any of the literals' contextual type at the literal expression sites, because both branches assign to the same untyped let and the contextual type only kicks in at the return statement.

Behavior with typescript@6.0

Compiles cleanly (exit 0, no diagnostics). Excess-property checking does not fire on the nested literals because they are assigned to an untyped let variable and the contextual type only flows in at return emailAddressData; (where excess-prop checking is no longer "fresh").

Behavior with tsgo

index.ts(29,17): error TS2353: Object literal may only specify known properties, and 'attributes' does not exist in type 'UsageData'.

Note that:

  • Only the first attributes: { "wd:Public": false } (the if (condition()) branch) is flagged. The matching attributes field in the else branch is not flagged.
  • The inner attributes: { "wd:Primary": true } on TypeData is not flagged in either branch.

That asymmetric reporting suggests tsgo is propagating the contextual type back into one of the conditional branches (the primary ? [...] : [] ternary that's inside the if-true branch) but not into emails.map((email) => ({...})) in the else branch. tsc treats both branches consistently.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions