typescript - Validează cheile în exces pe valoarea returnată de funcție
Să zicem că fac asta:
type Keys = 'a' | 'b' | 'c'
type Rec = { [K in Keys]?: number }
let rec: Rec = { a: 1, d: 4 }
Rezultă în:
Type '{ a: number; d: number; }' is not assignable to type 'Rec'.
Object literal may only specify known properties, and 'd' does not exist in type 'Rec'
Deci nu permite nici o cheie suplimentară pe un obiect.
Dar dacă fac următoarele:
type Func = () => Rec
const fn: Func = () => ({ a: 1, d: 4 })
TS este complet de acord cu asta, în ciuda faptului că funcția dată returnează cu siguranță un tip care nu este Rec.
Și, în același timp, utilizarea const fn: Func =() => ({ a: false, d: 4 }) oferă
Type 'boolean' is not assignable to type 'number | undefined'.(2322)
input.tsx(63, 12): The expected type comes from property 'a' which is declared here on type 'Rec'
Deci, de fapt validează o valoare returnată. Dar cumva nu îi pasă de cheile în exces.
Demonstrație
De ce se întâmplă acest lucru și există vreo modalitate de a nu permite chei suplimentare în valoarea returnată în acest caz?
Răspuns #1
Rețineți că {a: 1, d: 4} este de tipul Rec. Tipurile de obiect în TypeScript în general permit proprietăți suplimentare și nu sunt „exacte”, așa cum se solicită în microsoft/TypeScript#12936. Există motive întemeiate pentru aceasta, legate de subtipizare și atribuire. De exemplu:
class Foo {a: string = ""}
class Bar extends Foo {b: number = 123}
console.log(new Bar() instanceof Foo); // true
Rețineți că fiecare Bar este un Foo, ceea ce înseamnă că nu puteți spune „toate obiectele Foo au numai o proprietate a” fără a împiedica moștenirea și extinderea clasei sau interfeței. Și deoarece interface funcționează în același mod și deoarece sistemul de tipuri al TypeScript este structural și nu nominal, nici măcar nu trebuie să declarați un tip Bar pentru ca acesta să existe:
interface Foo2 {a: string};
// interface Bar2 extends Foo2 {b: number};
const bar2 = {a: "", b: 123 };
const foo2: Foo2 = bar2; // okay
Așadar, pentru bine sau pentru rău, suntem blocați cu un sistem de tipuri în care proprietățile suplimentare nu împiedică compatibilitatea tipurilor.
Desigur, acest lucru poate fi o sursă de erori. Așadar, în cazul în care atribuiți în mod explicit un obiect literal complet nou unui loc care așteaptă un anumit tip de obiect, există verificări pentru proprietăți în exces care se comportă ca și cum tipul ar fi exact. Aceste verificări se întâmplă doar în circumstanțe particulare, așa cum este în primul dumneavoastră exemplu:
let rec: Rec = { a: 1, d: 4 }; // excess property warning
Dar valorile returnate de funcții nu sunt în prezent una dintre aceste circumstanțe. Tipul valorii returnate este extins înainte ca orice verificare a proprietăților în exces să poată avea loc. Există o problemă destul de veche pe GitHub, microsoft/TypeScript#241, care sugerează că acest lucru ar trebui schimbat astfel încât valorile returnate de funcții să nu fie extinse în acest mod, și există chiar o implementare a unei posibile remedieri la microsoft/TypeScript#40311, dar a fost închisă, așa că s-ar putea să nu intre niciodată în limbaj.
Nu există modalități perfecte de a suprima proprietățile în exces în general. Sfatul meu este să accepți pur și simplu că obiectele pot avea chei în exces și să te asiguri că orice cod pe care îl scrii nu se va strica dacă acesta este cazul. Poți face lucruri care descurajează proprietățile în exces, cum ar fi acestea:
// annotate return type explicitly
const fn2: Func = (): Rec => ({ a: 1, d: 4 }) // excess property warning
// use a generic type that gets mad about excess properties
const asFunc = <T extends Rec & Record<Exclude<keyof T, keyof Rec>, never>>(
cb: () => T
): Func => cb;
const fn3 = asFunc(() => ({ a: 1, d: 4 })); // error! number is not never
Dar ele sunt mai complicate și se strică ușor, deoarece nimic nu te va opri să faci acest lucru, indiferent cât de mult încerci să protejezi tipul tău Func:
const someBadFunc = () => ({ a: 1, d: 4 });
const cannotPreventThis: Rec = someBadFunc();
Scrierea de cod care anticipează proprietăți suplimentare implică de obicei păstrarea unui array de chei cunoscute. Deci, nu faceți asta:
function extraKeysBad(rec: Rec) {
for (const k in rec) {
const v = rec[k as keyof Rec]; // bad assumption that k is keyof Rec
console.log(k + ": " + v?.toFixed(2))
}
}
const extraKeys = {a: 1, b: 2, d: "four"};
extraKeysBad(extraKeys); // a: 1.00, b: 2.00, RUNTIME ERROR! v.toFixed not a function
Fă asta în schimb:
function extraKeysOkay(rec: Rec) {
for (const k of ["a", "b", "c"] as const) {
const v = rec[k];
console.log(k + ": " + v?.toFixed(2))
}
}
extraKeysOkay(extraKeys); // a: 1.00, b: 2.00, c: undefined
Link de teren de joacă către cod
