typescript - afirmația despre null sau undefined nu este detectată într-o funcție încorporată
Am această funcție:
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;
}
}
Se presupune că aceasta verifică dacă ceva nu este null sau undefined. Dacă folosesc această funcție, TypeScript știe de asemenea că ceva nu este null sau undefined.
Totuși, atunci când folosesc această funcție într-o altă funcție astfel:
function has_email() {
const email = localStorage.getItem('email')
assertNonNullish(email, new Error("email does not exist"));
}
și apoi folosește funcția has_email pentru a verifica argumentul altei funcții:
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)
Se pare că TypeScript nu poate înțelege că rularea has_email() ar fi aceeași cu assertNonNullish(localStorage.getItem('email'))
Există vreo modalitate de a informa TypeScript? Aș dori să fac asta în acest mod pentru că trebuie să verific starea care trebuie încărcată din indexDB și mai întâi trebuie să aștept asta.
Răspuns #1
Din păcate, compilatorul TypeScript nu poate urmări suficient de bine ceea ce se întâmplă în codul tău pentru a furniza tipurile pe care le aștepți. Codul tău exemplu face în prezent două apeluri separate de forma
const email1 = localStorage.getItem('email');
const email2 = localStorage.getItem('email');
Și vă așteptați ca valoarea lui email1 și email2 să fie aceeași și, prin urmare, ele ar trebui să aibă același tip (de exemplu, dacă email1 nu este nul atunci nici email2 nu este). Dar TypeScript nu are o modalitate foarte bună de a codifica o astfel de așteptare; verificarea tipului lui email1 nu va determina compilatorul să concluzioneze nimic despre comportamentul ulterior al localStorage.getItem(). Și de fapt nu sunt sigur că ar trebui să concluzionați prea multe nici dumneavoastră; dacă cele două apeluri sunt în același fir și nu există nicio modificare intermediară a stocării, atunci poate că puteți spune că al doilea apel va returna aceeași valoare ca primul. Dar, în general, nu există o astfel de garanție.
Să eliminăm acea problemă prin efectuarea unui singur apel la localStorage.getItem('email') și o singură variabilă const numită email, a cărei valoare va fi astfel întotdeauna aceeași, indiferent când o verifici:
const email: string | null = localStorage.getItem('email');
function has_email() {
assertNonNullish(email, new Error("email does not exist"));
}
has_email();
Acum este absolut garantat 100% că orice cod care rulează după has_email() va avea acces la o variabilă email nenulă. Dar compilatorul încă nu o vede:
email.toUpperCase(); // error! Object is possibly null
De ce nu?
Aceasta este o limitare generală a TypeScript, așa cum este descris în microsoft/TypeScript#9998. Atunci când verifici sau afirmezi tipul unei valori pentru a restrânge tipul aparent, compilatorul folosește analiza fluxului de control pentru a ține evidența unde și când ar trebui să se aplice o astfel de restrângere. Dar această analiză funcționează doar în interiorul unui singur scope al funcției. Nu traversează limitele funcțiilor. Aceasta înseamnă că orice restrângere din interiorul unei funcții va fi inaccesibilă din exterior și viceversa. În exemplul nostru particular, restrângerea lui email care are loc în corpul funcției has_email() nu are efect asupra niciunui cod din exterior, și obținem o eroare.
Această limitare nu poate fi remediate cu ușurință. Ca ființă umană, pot să-mi imaginez rularea codului de mai sus de două ori, o dată presupunând că variabila email de tip string | null este un string, și o dată presupunând că este null. În primul caz totul este în regulă, iar în al doilea caz nu ajungi niciodată la vreo linie problematică. Pentru o uniune de doi membri și o funcție has_email() care este apelată o singură dată, acest lucru este suficient de ușor de făcut. Dar pur și simplu nu se scalează. Majoritatea funcțiilor sunt apelate de mai multe ori, iar dacă ai mai multe variabile de tip uniune, ar trebui să simulezi fiecare combinație posibilă de valori pentru fiecare apel de funcție. O dată am sugerat la microsoft/TypeScript#25051 că ai putea solicita acest comportament pe bază de opțiune (de exemplu, „te rog să faci acest tip de analiză doar pentru variabila email”), dar a fost respins; acest tip de lucru trebuie să se întâmple automat sau deloc.
De asemenea, nu există niciun concept de funcție de afirmație sau predicat de tip în care valoarea al cărei tip îl restrângi să nu fie unul dintre parametrii funcției (sau contextul this al unei metode). Nu poți să anotezi has_email(): asserts email is string, deoarece email nu este unul dintre parametrii pentru has_email(). Așadar, din nou, nu există nicio modalitate de a spune compilatorului că has_email() are vreo implicare asupra stării lui email.
Din aceste motive, trebuie să găsești o soluție în jurul acestuia.
Nu poți păstra algoritmul tău neschimbat fără să pierzi un pic din siguranța tipului. Poți suprima eroarea folosind afirmații de tip:
// assertion
email!.toUpperCase(); // okay
dar asta suprima eroarea chiar dacă uiți să apelezi mai întâi has_email().
Sau, dacă îți pasă mai mult de garanțiile legate de siguranța tipurilor decât de algoritmul tău specific, ai putea să faci refactorizare. În prezent, atribui email într-un loc și apoi îl verifici cu has_email() într-alt loc, lăsând posibilitatea de a accesa email acolo unde ar putea fi null. În schimb, putem scrie o versiune în care atribuirea și protecția tipului se întâmplă împreună:
function getDefiniteEmail(): string {
const email = localStorage.getItem('email');
assertNonNullish(email);
return email;
}
const email = getDefiniteEmail();
email.toUpperCase(); // okay
Acum, atunci când apelezi getDefiniteEmail(), știi că vei primi înapoi un string și nu null; deja a fost verificat! Aceasta ocolește întreaga problemă a analizei fluxului de control care traversează limitele funcțiilor, deoarece am codificat starea care ne interesează în tipul de returnare al getDefiniteEmail().
Link de teren de joacă către cod
