typescript - Den modifierade stängningsvariabeln ts kan inte upptäckas
Den online-demonstrationen
Jag ändrade flag-variabeln men TS hittade den inte. Är detta ett fel?
function fn () {
let flag = true
function f () {
// modify flag
flag = false
}
for (let i = 0; i < 10; i++) {
// error: This condition will always return 'false' since the types 'true' and 'false' have no overlap.
if (flag === false) { break }
f()
}
return flag
};
fn()
Svar #1
Se microsoft/TypeScript#9998 för ett auktoritativt svar på denna fråga.
Beteendet du ser är inte en bugg, utan en designbegränsning i TypeScript som inte riktigt kan åtgärdas särskilt enkelt. Du kanske har märkt att denna version av fn() fungerar bra:
function fn() {
let flag = true
for (let i = 0; i < 10; i++) {
if (flag === false) { break }
flag = false;
}
return flag
};
Här härleder kompilatorn att flag är av typen boolean, vilket i TS är en förkortning som motsvarar unionen true | false. När du tilldelar true till den vid initialisering, begränsar kompilatorn den till synes typen av flag från true | false till bara true. Efter denna rad antar kompilatorn att flag definitivt är true. Inuti for-loopens scope ser kompilatorn en annan tilldelning flag=false och vet att det är möjligt att komma in i loopens kropp med flag antingen true eller false, och därför återställs begränsningen till true i princip till boolean inuti loopen. Detta generella beteende kallas control flow analysis, där kompilatorn analyserar ordningen i vilken koden utvärderas så att den kan ge mer specifika typer till vissa värden i vissa scopes.
Tyvärr, som du noterar, bryts detta när du refaktoriserar den raden flag=false in i kroppen av f(). Kompilatorn nollställer inte förminskningen efter att f() har kallats, och den skenbara typen av flag är fortfarande true i hela loopen. Och därför klagar den felaktigt på att kontrollen flag === false är meningslös eftersom den tror att den alltid kommer att returnera false.
Men varför inser inte kompilatorn att att anropa f() påverkar den stängda flag-variabeln? Svaret är att man helt enkelt inte kan utföra kontrollflödesanalys med stängningmutationer på ett sätt som är både exakt och presterande. Du kan inte utföra inlining för varje funktionsanrop och analysera om implementeringen av varje funktion för varje anrop till den. Detta "skulle inte ens vara ens i närheten praktiskt", enligt TS-teamets utvecklingsledare (i ms/TS#9998, länkad ovan). Kanske skulle det kunna göras för ett leksaksexempel som i denna fråga, men för någon verklig kodbas skulle du få oöverkomligt långa kompileringstider... kanske till och med omöjligt långa, eftersom kontrollflödesanalys genom funktionsanrop nästan är som att simulera att programmet körs för varje möjlig uppsättning av indata.
Så noggrannhet är inte genomförbart. Istället måste kompilatorn göra optimerande antaganden, som i vissa fall visar sig vara felaktiga. TypeScript gör för närvarande det "optimistiska" antagandet att funktionsanrop inte påverkar tillståndet. Detta fungerar bra i många situationer, och det sparar definitivt kompileringstid. Men om funktionsanrop faktiskt påverkar tillståndet, då händer dåliga saker, som du ser här.
Den motsatta, "pessimistiska" antagelsen är att funktionsanrop kan påverka all tillstånd, och att varje funktionsanrop bör återställa alla inskränkningar. Detta skulle vara mycket värre i de flesta situationer, eftersom ofarliga funktionsanrop är otroligt mycket vanligare än tillståndsförändrande sådana.
Det är möjligt att något bättre skulle kunna göras här, men för tillfället är det så här det är. TypeScript är helt enkelt inte utrustat för att spåra tillståndsförändringar inuti funktionsanrop.
Så det är svaret på frågan som ställdes.
Men om du vill veta hur man förebygger detta felaktiga fel och du inte enkelt kan refaktorera för att undvika tillståndsändrande funktioner, är det enklaste sättet att helt förhindra styrflödesförträngning av flag. Du kan göra detta genom att ändra den initiala tilldelningen av flag så att kompilatorn inte inser att det är ett bokstavligt true-värde:
let flag = true as boolean;
Genom att skriva true as boolean utvidgar du typen av värdet true till true | false, och kompilatorn har nu ingen aning om vilket det initiala tillståndet för flag är. Därför är flag vid varje punkt i resten av scopet antingen true eller false enligt kompilatorn. Och att kontrollera flag === false accepteras.
Playground-länk till kod
