Hvorfor mislykkes kontrollflytanalyse når man bruker generiske typer i TypeScript
Jeg har en generisk type som tillater at en av egenskapene i et objekt kan være undefined, men aldri begge:
type RequireOne<T> =
| {
a: NonNullable<T>
b: NonNullable<T>
}
| {
a: NonNullable<T>
b: undefined
}
| {
a: undefined
b: NonNullable<T>
}
Nå, hvis jeg bruker denne typen når jeg erklærer funksjoner, fungerer det vakkert med konkrete typer:
const test1 = ({ a, b }: RequireOne<string>): string => a ?? b
Men det mislykkes hvis jeg prøver å gjøre den samme funksjonen generisk også:
const test2 = <T>({ a, b }: RequireOne<T>): T => a ?? b
// ERROR: Type 'NonNullable<T> | undefined' is not assignable to type 'T'.
Er dette forventet oppførsel fra TypeScript, og i så fall, hvorfor?
Svar #1
Generelt sett fungerer generiske typer og kontrollflytanalyse ikke veldig godt sammen. I dette spesifikke tilfellet ser det ut til at du har støtt på problemet rapportert i microsoft/TypeScript#50652, der en union ikke blir sett på som en diskriminert union når den tiltenkte diskriminant-egenskapen er generisk.
Her prøver du å diskriminere RequireOne<T> ved å sjekke a-egenskapen for null eller undefined, men typesjekkeren gjør det ikke.
Siden det er et problem med det, er atferden i det minste kjent, men det betyr ikke nødvendigvis at det er «forventet», gitt at problemet ligger på backloggen og er merket som «Trenger undersøkelse» (per i dag, 2023/08/04). Så det kan hende det ikke blir løst med det første, om noen gang.
Men igjen, generelt kan ikke TypeScript virkelig utføre vilkårlig høyereordensanalyse på generiske typer. NonNullable<T> er fra og med TypeScript 4.8 ekvivalent med krysstypen T & {}. For enhver spesifikk type T, vil kompilatoren forstå at T & {} er skillbar fra undefined. Men den kan ikke abstrahere slike ting til den generelle påstanden "for alle typer T, T & {} er skillbar fra undefined". Inntil og med mindre det blir hardkodet inn i kompilatoren av noen, vil du ha dette problemet.