typescript - kontroll av null eller undefined upptäcks inte i inbäddad funktion


Jag har denna funktion:

function assertNonNullish<TValue>(
  value: TValue,
  error?: Error
): asserts value is NonNullable<TValue> {
  if (value === null || value === undefined) {
    error ??= new Error("Something does not exist");
    throw error;
  }
}

Detta ska kontrollera om något varken är null eller undefined. Om jag använder denna funktion vet TypeScript också att något varken är null eller undefined.

Men när jag använder denna funktion i en annan funktion på följande sätt:

function has_email() {
  const email = localStorage.getItem('email')
  assertNonNullish(email, new Error("email does not exist"));
}

och använd sedan funktionen has_email för att kontrollera argumentet för en annan funktion:

function print_email(email: string) {
  console.log(email);
}

has_email() // Throws if email is not available but ts doesn't pick up on this
const email = localStorage.getItem('email')

print_email(email)

Det verkar som att TypeScript inte kan lista ut att körning av has_email() skulle vara samma sak som assertNonNullish(localStorage.getItem('email'))

Finns det något sätt att låta TypeScript veta? Jag skulle vilja göra det på det här sättet eftersom jag måste kontrollera ett tillstånd som måste laddas från indexDB och jag måste först vänta på det.

Svar #1

Tyvärr kan TypeScript-kompilatorn inte spåra vad som händer i din kod tillräckligt bra för att tillhandahålla de typer du förväntar dig. Ditt exempel kod gör för närvarande två separata anrop av formen

const email1 = localStorage.getItem('email');
const email2 = localStorage.getItem('email');

Och det är din förväntning att värdet av email1 och email2 kommer att vara detsamma, och därför bör de ha samma typ (t.ex. om email1 inte är nullish så är inte heller email2 det). Men TypeScript har inte ett bra sätt att koda en sådan förväntning; att kontrollera typen på email1 kommer inte att få kompilatorn att dra några slutsatser om efterföljande beteende för localStorage.getItem(). Och faktiskt är jag inte säker på att du bör dra många slutsatser heller; om de två anropen är i samma tråd och det inte sker någon förändring av lagringen däremellan så kanske du kan säga att det andra anropet kommer att returnera samma värde som det första. Men i allmänhet finns det ingen sådan garanti.


Låt oss ta bort det problemet genom att ha ett enda anrop till localStorage.getItem('email') och en enda const-variabel som heter email vars värde därför alltid kommer att vara detsamma oavsett när du kontrollerar det:

const email: string | null = localStorage.getItem('email');

function has_email() {
  assertNonNullish(email, new Error("email does not exist"));
}

has_email();

Nu är det absolut 100 % garanterat att all kod som körs efter has_email() kommer att ha tillgång till en icke-null email-variabel. Men kompilatorn ser det fortfarande inte:

email.toUpperCase(); // error! Object is possibly null

Varför inte?


Detta är en generell begränsning i TypeScript, som beskrivs i microsoft/TypeScript#9998. När du kontrollerar eller hävdar typen av ett värde för att begränsa dess uppenbara typ, använder kompilatorn kontrollflödesanalys för att hålla reda på var och när en sådan begränsning ska gälla. Men denna analys fungerar endast inom ett enda funktionsomfång. Den korsar inte funktionsgränser. Det innebär att alla begränsningar inom en funktion inte kommer att vara tillgängliga utanför, och vice versa. I vårt konkreta exempel har begränsningen av email som sker inom kroppen av has_email() ingen effekt på någon kod utanför, och vi får ett fel.

Denna begränsning kan inte enkelt åtgärdas. Som människa kan jag föreställa mig att köra koden ovan två gånger, en gång med antagandet att email-variabeln av typen string | null är en string, och en annan gång med antagandet att den är null. I det första fallet är allt bra, och i det andra fallet når man aldrig någon problematisk rad. För en union av två element och en has_email()-funktion som kallas en gång, är detta tillräckligt enkelt att göra. Men det skalar helt enkelt inte. De flesta funktioner kallas flera gånger, och om du har flera unionstyperade variabler skulle du behöva simulera varje möjlig kombination av värden för varje funktionsanrop. Jag föreslog en gång på microsoft/TypeScript#25051 att man kunde be om detta beteende på frivillig basis (t.ex. "vänligen gör bara denna typ av analys för email-variabeln"), men det avslogs; den här typen av saker måste ske automatiskt eller inte alls.


Det finns heller inget begrepp av en assertionsfunktion eller typ-predikat där värdet vars typ du förminskar inte är en av parametrarna till funktionen (eller this-kontexten för en metod). Du kan inte annotera has_email(): asserts email is string, eftersom email inte är en av parametrarna till has_email(). Så återigen, det finns inget sätt att tala om för kompilatorn att has_email() har några konsekvenser på tillståndet för email.


Av dessa skäl behöver du arbeta runt det.

Du kan inte behålla din algoritm oförändrad utan att förlora viss typkontroll. Du kan undertrycka felet med typangivelser:

// assertion 
email!.toUpperCase(); // okay

men det undertrycker felet även om du glömmer att först kalla has_email().

Eller, om du bryr dig mer om typ-säkerhetsgarantier än om din specifika algoritm, kan du refaktorera. För närvarande tilldelar du email på ett ställe och sedan kontrollerar det med has_email() på ett annat ställe, vilket lämnar möjligheten öppen att du kan komma åt email där det kan vara null. Istället kan vi skriva en version där tilldelningen och typkontrollen sker tillsammans:

function getDefiniteEmail(): string {
  const email = localStorage.getItem('email');
  assertNonNullish(email);
  return email;
}

const email = getDefiniteEmail();
email.toUpperCase(); // okay

Nu när du anropar getDefiniteEmail() vet du att du kommer att få tillbaka en string och inte null; det har redan kontrollerats! Detta kringgår hela problemet med kontrollflödesanalys över funktionsgränser, eftersom vi har kodifierat det tillstånd vi bryr oss om i returtypen för getDefiniteEmail().

Playground-länk till kod