microsoft / microsoft/TypeScript

Unpredictable behavior when trying to infer a generic type from a union function parameter

Aperta
#59,046 3 commenti 1 reazione 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Bug Domain: check: Type Inference Help Wanted
Lingua principale
Go
Stelle
111k
Fork
14.4k
Merge medio
1g 19h
PR unite (30g)
117

Descrizione

🔎 Search Terms

"parameter generic inference", "union order", "ts2345", "union generic parameter"

🕗 Version & Regression Information
  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about unions and generics though I went through it all in general
⏯ Playground Link

https://www.typescriptlang.org/play/?#code/C4TwDgpgBAygrgYwRAzimwCGw4qgXigCYAGEqAH2JIEYBuAKFEigEkA7AN0wBsBLACYZsuAlAAsZShNrTJROSQDMi8YybhoAWRAAlVGAD27FBAA8AFQA0sKBAAewCOwF54SVOiw48VDt34hb1wAPjEAbwYoKBRglAAuWEZowwBrRJg7R2dXWERkNGEfKAB+KGAAJzhoDKynFzx-XkEi0TKAM15TKESAI0NDHghMdmSoACsUYwAKAEpEgAUKwwBbPlNLEMYAX3UHIwrgKARjWKgKg1OIAAkRgSGKsUwUEHYEKEsrEOmo88uTCDxX7RKg6fQoIwAz55DyFOIhYHSMH-DbhOwVZYVLSeTAAc0BMUqfHYuKg2xsTUCrRQCNmBDCkWifHaUGmAEILhCrgA6NJ0xnRY6nI5oiAYwxYnH4slPADumD4R05kNM3MmM1mY2iwAAFstZVB2BADQBRcUVaZizHYtB4iCa37bX4XHAVdh-LkAtVTdhzHbqAD0AagAANwoJErEKsTSdsQ1AdWLoAgRlBetARiBdTGbL04Ecw+jrVKCVGY2T4wwg1BZXweDwoLx5SA8OmoGBOc5gDZxrgjuxDP2IAUUJhozwQDXFTrQ5JxCGbGtcTqjm3M2wuM0giIUBoWAtDGg+L0hhZNCgAIIEX6gvQo8xoiOE6Mksk2UgkBEgqDIz2oosSjao74pGRKvuSMjiFsDB7tAB5HieEBnpAKAAELXt+v4qg+AGSraIHPuWEFzl+SJ3n+OFPmW4HvmQ0GwVAABqW7gthYhYVcZiPgIoEvrGtGfowzDQGamKsVc7HkdhXG4UBdq8URNgkUJmhQPBKDHqe54AMJiKJEriQC0jMYEhmmOoJwmEcAjYJgV6EJg8qKh62G3C4DzTOE2yNng6maUh54Xg6gohaFYXhRFgrVgAejWEqpHgzxZJACBOAIDCWWcNlYOhDlOUq95ufcYqed5SV+YhyGoKhwWRXV9WhTFNbAO0JQZcKUDZZgul5QqBUUUVHleT5amHhplU6bVDXTZFTUDgazmyvFiV4Psw5pdym1pvmNY6iAJRAA

💻 Code
type SuccessStatus = 200 | 201;
type InvalidStatus = 400 | 401 | 402 | 403 | 404;

type MyResponse<T, S extends SuccessStatus | InvalidStatus> = {
  status: S;
  ok: S extends SuccessStatus ? true : S extends InvalidStatus ? false : boolean;
  json(): Promise<T>;
};

export const responseHandler = async <T,>(
  response:
    | MyResponse<T, SuccessStatus>
    | MyResponse<{ errorMessage: string }, InvalidStatus>
) => {
  if (!response.ok) {
    const { errorMessage } = await response.json();
    throw new Error(errorMessage);
  }
  return response.json();
};

// `{id: string }` here can be anything, but `{ errorMessage: string }` 
// will always be present, just not necessarily with `404`, might be any InvalidStatus
type PossibleTypesA =
  | MyResponse<{ id: string }, 200>
  | MyResponse<{ errorMessage: string }, 404>;

type PossibleTypesB =
  | MyResponse<{ errorMessage: string }, 404>
  | MyResponse<{ id: string }, 200>;

type ValidResponse = MyResponse<{ id: string }, 200>;
type ErrorResponse = MyResponse<{ errorMessage: string }, 404>;
type PossibleTypesC = ErrorResponse | ValidResponse;

const dataA = await responseHandler({} as PossibleTypesA);
                                    // ^ works as expected
const dataB = await responseHandler({} as PossibleTypesB);
                                    // ^ wtf?
const dataC = await responseHandler({} as PossibleTypesC);
                                    // ^ now it works as expected... but why?
🙁 Actual behavior

responseHandler in the snippet above throws a ts2345 error when a union type with a swapped order is passed as an argument. Until now I thought a union type a | b is equivalent to b | a but this doesn't seem to be the case. Curiously, this is no longer an issue if the types that are part of the union are aliased, as can be seen in example C (dataC and PossibleTypesC).

🙂 Expected behavior

dataB should not throw an error.

Additional information about the issue

There is an even crazier example and perhaps it's a better proof that this is a bug.

type SomeType = { id: string };

type PossibleTypesA =
  | MyResponse<{ id: string }, 200>
  | MyResponse<{ errorMessage: string }, 404>;

type PossibleTypesB =
  | MyResponse<{ errorMessage: string }, 404>
  | MyResponse<SomeType, 200>;

const dataA = await responseHandler({} as PossibleTypesA);
                                    // ^ works as expected
const dataB = await responseHandler({} as PossibleTypesB);
                                    // ^ does not work but that's why I'm reporting it

Playground Link

type SomeType = { id: string };

type PossibleTypesA =
  | MyResponse<SomeType, 200>
  | MyResponse<{ errorMessage: string }, 404>;

type PossibleTypesB =
  | MyResponse<{ errorMessage: string }, 404>
  | MyResponse<SomeType, 200>;

const dataA = await responseHandler({} as PossibleTypesA);
                                    // ^ works as expected
const dataB = await responseHandler({} as PossibleTypesB);
                                    // ^ it works now???

Playground Link

This is pure insanity to me as the only thing I changed between these two examples is whether PossibleTypesA uses SomeType or not. It should not in any circumstance affect dataB, so I'm extremely confused what is happening here.

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Direzione di ricerca

Inizia dalle riproduzioni collegate di TypeScript Playground e confronta le chiamate a responseHandler per PossibleTypesA, PossibleTypesB e PossibleTypesC. Analizza l’inferenza generica per le unioni i cui membri sono scritti in ordini diversi o usano alias. Il lavoro è completato quando ordinamenti equivalenti delle unioni non producono più errori ts2345 incoerenti, mentre gli esempi mostrati continuano a essere inferiti correttamente.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
typescript
Ambito
compilers
Tipo di issue
Bug
Difficoltà
4/5
Tempo stimato
3-5 giorni
Stato di attività
Ferma
Chiarezza
Abbastanza chiara
Idoneità per principianti
28/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.