microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.75k stars 12.46k forks source link

Variables (extracted from a discriminated union object) lose narrowed types after being exported #59652

Open laam-egg opened 2 months ago

laam-egg commented 2 months ago

🔎 Search Terms

discriminate union export destructure destructuring

🕗 Version & Regression Information

⏯ Playground Link

https://www.typescriptlang.org/play/?#code/GYVwdgxgLglg9mABAQwhApgBygZSgJxjAHMB5MAGwE8AKAfQA8AuRAZwKOIEpEBvRAL4AoUJFgIUaLFAByIChQCCYACYBVVemBF0K8tXrNEYeRUQAfROBVadKnv2Ejw0eEgC2IKCGQKqAUTBkACMKXQAFZBh8AB4AFQA+GgA3XxB0FjiuFl4hRER0INDdFgJ0gBo8xFSKdJYZBDkFELD4hMqBCz4qwpaSxGBfVnRK-Jq641Mu61swXQ7u-JhgRBoTBUQAXm3qtPQLSxntOZUtnfH0Byr8-HRvfCR+XuKVFkGKYfLd2ozJjYEANxVTroD77XL5G53EAPPgFIphV6IMojb7pQRA-LCJwQBDsOHPREsfxfC4sABqgi2iE83l81ECfRUkWiNAARKw4O59uxCCQ0eg2VwgUJlqt-Fd8uSAYgAPSyxAAC3Qt0QMCgatYbA4JC+cAA1lVUBhsHg+WRKLRycKhCCwYtENK5Qrlar1Zq-mZDppjvNEAajVJsE0lKoNDZfXpLTRrUCnOgGJg4PgNbiwPingi-RcqZsaV4fH5GS8Wfh2ZzudrzQKhUJRSsaITdJKBTL5UqVft3TAtbzOF11l6rD67F8AO7oaIqQMm3A6i0GC7C52IfwAJTXpDXtoK9ohrZXrq7GqGcA9ff5lkH0xHJxn0hDynUt90+loS7bCvXm+3wiAA

💻 Code

function acceptStringOnly(_x: string) { }
function acceptNullAndUndefinedOnly(_x: null | undefined) { }

function mutuallyEnabledPair<T>(value: T): {
  enabled: true,
  value: NonNullable<T>,
} | {
  enabled: false,
  value: null | undefined,
} {
  if (null === value || undefined === value) {
    return { enabled: false, value: null };
  } else {
    return { enabled: true, value };
  }
}

const { enabled: E, value: V } = mutuallyEnabledPair("some string value");

if (E) {
  V; // here it is string, ok
  acceptStringOnly(V);
} else {
  V; // here it is null | undefined, ok
  acceptNullAndUndefinedOnly(V);
}

export const { enabled, value } = mutuallyEnabledPair("some string value")

if (enabled) {
  value; // here it is string | null | undefined, weird
  acceptStringOnly(value); // ERROR
} else {
  value; // here it also is string | null | undefined
  acceptNullAndUndefinedOnly(value); // ERROR
}

🙁 Actual behavior

When variables are destructured from an object of a discriminated union type and they are exported, those variables lose their narrowed types defined by the discriminated union type. Meanwhile, if the variables are NOT exported, their narrowed types will be preserved, which is the expected behavior, whether the variables are exported or not.

🙂 Expected behavior

Preserve narrowed types for variables that are:

Additional information about the issue

I guess this issue may be more or less related to this one.

MichalMarsalek commented 2 months ago

Simplified repro:

declare function mutuallyEnabledPair(): {
  discriminator: true,
  value: string,
} | {
  discriminator: false,
  value: null | undefined,
}

export const { discriminator, value } = mutuallyEnabledPair()

if (discriminator) {
  value;
}
MichalMarsalek commented 2 months ago

In the function getCandidateDiscriminantPropertyAccess, symbol.valueDeclaration is undefined in the export version & isn't undefined without the export. This leads to the narrowing using the discriminant to not work.

https://github.com/microsoft/TypeScript/pull/59673 Seems to fix it, altough I doubt this is a proper solution.