microsoft / microsoft/TypeScript

Inconsistent narrowing of adjacently tagged unions

Ouverte
#60,685 1 commentaire 2 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

Experimentation Needed Suggestion
Langage dominant
Go
Étoiles
111k
Forks
14.4k
Merge moyen
1 j 19 h
PR mergées (30 j)
117

Description

🔎 Search Terms

narrow adjacently tagged union

🕗 Version & Regression Information

This changed between versions v4.8.4 and v4.9.5:

Prior to v4.9.5, TypeScript would work the same both for SomeInterface | string | undefined and SomeInterface | string, which is still undesired behavior, but it is consistent.

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.8.0-dev.20241204#code/JYOwLgpgTgZghgYwgAgMpjpAPAFQJ4AOEAfMgN4BQyyAzhpAFzL5EDcFAvhRaJLIigCqNaADk4AWxSVqMYFDpM6UUAHN21ADZw6AfiVgVIdZ24B6M8gDCcEAHIwyAK4hgAexDIQcKFDcB3ZDcYWkM1ZAAfIIAjACsIBDAGbgQPOi9JCHRMCABGZABecg5kHWcQAGsQAM8ymWQANzhNJwgDIxNqOhymbOxlNWJ2Eqj6ppa25GExTI1QnrR6CCxpqHEpIc52HhCACjBCCGCMqT68gDpx1sKCooAiAeM7gEpyKhOspdzz7shaTGANDkEBoixyWEeqk2XAoFmQACEnI5gI5-G4oBVQcAQgBJUoAE3x5XxEDkIAgRLAbmQYAAFigJMAAB7JCipEDpbynJYAJkKxVKoJcVRqgre1CukxcJLJFLmv0mZyw0tJoAp0Mi4sazVa7TU8qWvSWELCxg1o3ekqYq3WEANCyVNsy0O22OQ+0Oxy5nxyPMuOpQt3ukJeWu9Zz9Cv+YEBwNBSsh0O45ks8I8TlBuSYAHkCAQ3DQUSh2fiUe5PID7I52YW6BBwDS3G5WW6PUQvZkzt9JTd7m44gkwHdkAAyEfIO5yBRD5CgD5d-0TV71cNfH5LaOx4AgsHYJ0bYbcVsHdshVe+xfXIMT-vxRLDscTqd0Ydz8+QP2S5fvd8QSMbmgASBbd42NfcSEPWFU3TUEeSYKx6QQCpwgzcIAANATQ5B8TcEEq2QNEMSCTxojcOlynLGgWz2QF0A6XZf27ANnm-ahGPXHJN2AncE1NKFINbWi+IYzteUvCAWLDUSLyjQCY240DwUTSCYBcRJy1nGg6LUXYrVKEA8GeJge0BUIOi1KAIDAJwoE8E8jhCHtrwePi7lMZM4QAQRAIkYHRGl6WQSzUigfEABoAtM0y6RQezkDxDMKUbZBoicYBNEpekRAotJWTigARBI3AkfMRHxFhlgq0gigq5AICZSAfNBCr3l0LVqAlAMmAquYOvmRhd0qw5Nj6rhqCYckGmgVggA

💻 Code
interface State<Type> {
  state: Type;
}

interface UserName {
  first: string;
  last?: string;
}

// Can't union narrow of string | object:

const nameState1 = {} as unknown as {
  value: string;
  state: State<string>;
} | {
  value: UserName;
  state: State<UserName>;
};

if (typeof nameState1.value === "string") {
  nameState1.state satisfies State<string>;
  //               ^^^^^^^^^
  // Type 'State<string> | State<UserName>' does not satisfy the expected type 'State<string>'.
  //   Type 'State<UserName>' is not assignable to type 'State<string>'.
  //     Type 'UserName' is not assignable to type 'string'.(1360)
}

// But it works if I add undefined to the mix:

const nameState2 = {} as unknown as {
  value: undefined;
  state: State<undefined>;
} | {
  value: string;
  state: State<string>;
} | {
  value: UserName;
  state: State<UserName>;
};

if (typeof nameState2.value === "string") {
  nameState2.state satisfies State<string>;
}
🙁 Actual behavior

nameState1 won't narrow down to State<string>, but when undefined is in the union (nameState2) it does narrow down with the same condition.

🙂 Expected behavior

I would expect nameState1 to correctly narrow down to State<string> or at least not to change behavior when adding undefined to the mix.

Additional information about the issue

This inconsistency is not consistent across different narrowing methods too:

// Bonus 1: Opposite condition isn't consistent too:

if (typeof nameState1.value === "object" && "first" in nameState1.value) {
  nameState1.state satisfies State<UserName>;
  //               ^^^^^^^^^
}

if (typeof nameState2.value === "object" && "first" in nameState2.value) {
  // No type error!
  nameState2.state satisfies State<UserName>;
}

// Bonus 2: Checking using `is` doesn't work on both unions:

if (isString(nameState1.value)) {
  nameState1.state satisfies State<string>;
  //               ^^^^^^^^^
}

if (isString(nameState2.value)) {
  nameState2.state satisfies State<string>;
  //               ^^^^^^^^^
}

function isString(value: any): value is string {
  return typeof value === "string"
}

Guide de contribution

Ouvrir le guide de contribution

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Piste de recherche

Commencez par le TypeScript Playground lié et reproduisez les exemples nameState1 et nameState2, y compris les vérifications typeof, property et user-defined type-predicate. Comparez le comportement de narrowing obtenu aux types attendus ; le travail est terminé lorsque la propriété d’état associée est réduite de manière cohérente pour des cas d’union équivalents.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
typescript
Domaine
compilers
Type d'issue
Bug
Difficulté
4/5
Temps estimé
3-5 jours
Activité
À l'abandon
Clarté
Plutôt claire
Accessibilité débutants
42/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.