Când se atribuie callback-uri în TypeScript, parametrii sunt atribuiți în direcția opusă a callback-ului în sine. De ce se întâmplă asta?
Când se apelează o funcție cu un argument, tipul argumentului este atribuit tipului parametrului. Cu alte cuvinte, tipul argumentului trebuie să fie egal cu, sau mai restrâns decât, tipul parametrului.
Exemplu :
const foo = (bar: string | number) => {
console.log(bar);
}
foo('bar');
De mai sus funcționează bine deoarece 'bar' (un tip literal de șir) poate fi atribuit la string | number.
Acum, ia în considerare un scenariu de apel invers:
const foo = (bar: (baz: string | number) => void) => {
bar('baz');
// We should probably also have a call with a numeric parameter here, like bar(42),
// since the callback signature implies that it is also called that way.
}
const bar = (baz: string) => {}
foo(bar);
Primim acest mesaj de eroare la bar('baz'):
Argument of type '(baz: string) => void' is not assignable to parameter of type '(baz: string | number) => void'.
Types of parameters 'baz' and 'baz' are incompatible.
Type 'string | number' is not assignable to type 'string'.
Type 'number' is not assignable to type 'string'.ts(2345)
După cum se vede, în cazul callback-ului, TS încearcă să atribuie tipul (baz: string) => void (tipul argumentului) la (baz: string | number) => void (tipul parametrului). Acest lucru era de așteptat.
Parametrul callback-ului, totuși, este atribuit în direcția opusă: Tipul string | number (parametru al argumentului callback) este atribuit tipului string (parametru al parametrului callback).
Cum se face că direcția de atribuire a parametrilor în callback este inversată față de scenariul fără callback?
Răspuns #1
Dacă observi că atribuțiile merg „în direcția opusă”, ai putea spune că acestea „variază contrar”. Într-adevăr, tipurile de funcții sunt contravariante în tipurile lor de intrare. Astfel, o funcție de forma (x: X) => void poate fi atribuită la (y: Y) => void dacă Y poate fi atribuit lui X, nu invers. În special, (baz: string | number) => void poate fi atribuit lui (baz: string) => void, dar (baz: string) => void nu poate fi atribuit lui (baz: string | number) => void.
Acest lucru poate fi văzut prin adăugarea mai multor funcționalități codului dvs. care se conformează tipurilor. În primul rând, în interiorul foo(), parametrul bar ar trebui să accepte string | number, așa că puteți apela bar() cu orice argument de tip string sau numeric doriți. Să ne asigurăm că facem ambele:
const foo = (bar: (baz: string | number) => void) => {
bar('baz');
bar(123); // <-- this is okay too
}
Atunci bar acceptă doar un argument baz de tip string, ceea ce înseamnă că ar trebui să fie în regulă să tratezi baz ca pe un șir, cum ar fi apelarea metodei sale toUpperCase():
const bar = (baz: string) => { baz.toUpperCase() } // <-- this is okay too
Acum,
foo(bar); // error
// ~~~ <-- Type 'string | number' is not assignable to type 'string'.
îți dă aceeași eroare de compilator (și nimic nu s-a schimbat din punctul de vedere al sistemului de tipuri). Datorită contravarianței, mesajul de eroare îți spune că (baz: string) => void nu poate fi atribuit lui (baz: string | number) => void deoarece string | number nu poate fi atribuit lui string.
Și dacă rulezi efectiv acest cod fără să corectezi eroarea, vei primi o eroare de execuție că baz.toUpperCase nu este o funcție.
Faptul că o funcție poate gestiona un string nu implică că poate gestiona un string | number.
Pe de altă parte, este perfect în regulă să lărgești intrarea funcției:
const qux = (baz: string | number | boolean) => {
if (typeof baz === "string") {
console.log(baz.toUpperCase())
} else if (typeof baz === "number") {
console.log(baz.toFixed())
} else {
console.log(baz === true)
}
}
foo(qux); // okay
Asta funcționează pentru că orice funcție care poate gestiona un string | number | boolean poate, cu siguranță, gestiona un string | number. Nu apare nicio eroare la rulare pentru că în interiorul qux() nu se scrie pur și simplu baz.toUpperCase() fără a verifica dacă este un string. (Și dacă ai încerca să faci asta, ai primi o eroare de compilare acolo spunând că number și boolean nu au metode toUpperCase().)
Așa că de aceea lucrurile sunt alocate în direcția opusă pentru intrările funcției. Ele variază invers din cauza direcției în care se mișcă datele. O bucată de date nu trebuie să fie mai lată decât cutia în care intră. Deci poți întotdeauna să faci acea cutie mai mare sau datele mai mici, dar nu invers. Pentru parametrii funcției, datele vin în, ceea ce înseamnă că funcția poate face parametrul de intrare mai lat, dar nu mai îngust.
Link de teren de joacă către cod
