Varför kan TypeScript inte avgöra att en variabel är definierad efter en if-sats som kollar efter undefined returnerar aldrig


Jag kompilerar i TypeScript 3.6.2 med strictNullChecks aktiverat.

Säg att jag deklarerade en variabel som kan vara odefinierad:

let filename: string|undefined;

Sedan kan en återanropsfunktion tilldela ett värde till det, eller lämna det odefinierat:

doIt(() => filename = "assigned");

Nu kontrollerar jag om återuppringningen är tilldelad till filename; annars,filenameär odefinierad och jag avslutar programmet (returvärde avnever`):

if (filename === undefined) {
  process.exit(0);
}

Om detta if-villkor är falskt, betyder det att filename måste ha ett giltigt strängvärde, eller hur? Slutligen försöker jag använda min mest-definitivt en sträng:

console.log(filename.toUpperCase());

Men jag får ett fel:

source/repro.ts:6:13 - error TS2532: Object is possibly 'undefined'.

6 console.log(filename.toUpperCase());
              ~~~~~~~~


Found 1 error.

Så vitt jag förstår, eftersom if-satsen ovan har en never-retur, betyder det att programmet avslutas innan det kan nå de följande raderna som använder filename; därför måste filename vara en sträng! Missar jag något här? Varför tror TypeScript fortfarande att filename fortfarande kan vara undefined efter never-returen?

För reproduktion, här är hela programmet:

let filename: string|undefined;
doIt(() => filename = "assigned");
if (filename === undefined) {
  process.exit(0);
}
console.log(filename.toUpperCase());
function doIt(fn: () => void) {
  fn();
}

Notera: Jag kan lösa mitt problem i mitt riktiga program eftersom jag kan initiera filename="", och kontrollera det i if-satsen. Jag undrar dock varför detta specifika tillvägagångssätt inte fungerar.

REDIGERA: här är min tsconfig.json. Jag försökte detta exempel i en helt ny mapp UTAN en tsconfig.json och jag kunde inte återskapa detta fel. Kanske är det något som inte stämmer i min tsconfig, men jag har inte fastställt det än:

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es2017",
    "moduleResolution": "node",
    "sourceMap": true,
    "outDir": "dist",
    "declaration": true,
    "alwaysStrict": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "noImplicitAny": true,
    "strictBindCallApply": true,
    "strictFunctionTypes": true,
    "strictNullChecks": true,
    "noUnusedLocals": true
  },
  "include": [
    "source/**/*.ts"
  ]
}

Svar #1

Detta är beteendet i Typescript 3.6.3 och tidigare, men det fungerar faktiskt på det sätt du vill i version 3.7.2; här är en Playground-länk för att se själv. Om du växlar fram och tillbaka mellan versioner med menyn, dyker felet upp och försvinner.

Om detta är nödvändigt för ditt projekt kan du uppgradera Typescript.


I grund och botten var problemet att kontrollflödesgrafen bestäms innan typkontroll sker, så vid den tidpunkt då CFG bildas (och räckvidd kontrolleras) är faktumet att exit returnerar never inte tillgängligt, och därmed fortsätter CFG-grenen där exit anropas till koden efter if-satsen, där variabeln befinner sig i ett möjligen odefinierat tillstånd.

Detta togs upp som ett problem på GitHub i december 2016, och enligt ett svar i en annan tråd,

#12825 Generalisera hanteringen av never för returvärden

  • Kontrollflödesgrafen bildas under bindning, men vi har ännu ingen typdata
  • Vi skulle kunna lagra alla anrop vid varje flödeskontrollpunkt och sedan kontrollera dem för aldrig återvändande och kontrollera denna information för att beräkna typer

    • Dyr!
  • Korrekt analys skulle kräva flera iterationer

Så dessa är några av anledningarna till att det kanske inte har lösts i versionerna 3.6.3 och tidigare.

Svar #2

Så huvudproblemet här är att i 3.6 påverkade aldrig-återkommande funktioner inte kontrollflödesanalysen. Denna funktion implementerades i 3.7 genom denna PR.

Om du kör din kod (med några av typerna från noddefinitionerna kopierade) kan vi se att det kommer att fungera i 3.7 men inte i 3.6

Även pilfunktionen som sätter variabeln har egentligen ingen påverkan på resultatet. Typescript kommer inte att göra någon kontrollflödesanalys på hur en callback anropas från doIt. Detta beskrivs i detta problem.

Var bara försiktig med 3.7 och funktioner som aldrig returnerar, villkoren för att de ska kunna delta i styrflödet är ganska strikta:

Ett funktionsanrop analyseras som ett assertionsanrop eller ett aldrig-returnerande anrop när

  • anropet sker som ett uttrycksuttryck på toppnivå, och
  • anropet specificerar ett enskilt identifierare eller en punktseparerad sekvens av identifierare för funktionsnamnet, och
  • varje identifierare i funktionsnamnet refererar till en enhet med en uttrycklig typ, och
  • funktionsnamnet löses till en funktionstyp med en returntyp som garanterar eller en explicit aldrig-returtyp-anteckning.

En enhet anses ha en explicit typ när den deklareras som en funktion, metod, klass eller namnrymd, eller som en variabel, parameter eller egenskap med en explicit typanteckning. (Denna särskilda regel finns för att kontrollflödesanalys av potentiella assertionsanrop inte ska cirkulärt utlösa ytterligare analys.)

Så funktionsuttryck kanske inte deltar i CFA om det inte har en tydlig annotation

const exit = () =>  {
  throw new Error()
}

let filename: string | undefined;
if (filename === undefined) {
  exit();
}
console.log(filename.toUpperCase()); // error 

Playground-länk Med en tydlig kommentar fungerar det:

const exit: () => never = () =>  {
  throw new Error()
}

let filename: string | undefined;
if (filename === undefined) {
  exit();
}
console.log(filename.toUpperCase()); // error 

Lekplatslänk

Relaterade artiklar