typescript - Cum să restricționezi tipul pentru o proprietate în funcție de altă proprietate din același obiect?
Iată interfața pentru un parametru
este un obiect cu următoarea semnătură:
interface IRenderDataToTable<T> {
data: T[];
titles: { [key in keyof T]: string };
excludeProperties: (keyof T)[];
}
Date exemplu:
dataeste acesta:{id: 1, name: 'a', age: 2}excludePropertiesva fi['id']- deci,
titlesar trebui să fie{name: 'Nume Utilizator', age: 'Vârstă Utilizator'} - Deoarece
ida fost exclus
Problemă
Verificarea tipurilor funcționează în mare parte, doar o problemă,
titles: { [key in keyof T]: string };
excludeProperties: (keyof T)[];
Vreau ca titles să conțină keyof T, dar să excludă key care se află în excludeProperties. Ceva de genul acesta:
titles: { [key in keyof Pick<T, Exclude<keyof T, excludeProperties>>]: string };
Declarația de mai sus va conduce la o eroare: [ts] Cannot find name 'excludeProperties'. [2304], care apare deoarece TS nu poate găsi unde se află excludeProperties.
Cum se face asta? Mulțumesc
Răspuns #1
Dacă este posibil să faci acest lucru depinde de răspunsul la această întrebare:
excludePropertiess-ar schimba la rulare? Sau te aștepți să poți schimbaexcludePropertiesla rulare?
Dacă răspunsul este „Da”, atunci este imposibil să faci asta și motivul va fi explicat mai târziu.
Să discutăm mai întâi soluția atunci când răspunsul este „Nu”.
Dacă excludeProperties va fi imuabil în timpul rulării, având în vedere că scopul său este de a restricționa tipul lui titles, este mai degrabă o constrângere de tip decât o valoare.
Constrângere de tip vs valoare (înțelegerea mea)
constrângere de tip: A spune că "titlesnu ar trebui să aibă chei 'name' și 'age'". Este o regulă și este abstractă. Nu este nevoie să existe o variabilă concretă care să păstreze['name', 'age'].
valoare: Variabilă concretă cu valoarea['name', 'age']. Ea există în memoria calculatorului și este concretă. Poți să iterezi peste ea, să adaugi noi elemente și chiar să elimini elemente din ea. În timp ce nu ai nevoie de ea dacă nu trebuie să iterezi, să adaugi sau să elimini elemente.
Așadar, putem declara tipul ca
interface Foo<T, E extends keyof T> {
data: T[],
titles: { [key in Exclude<keyof T, E>]: string }
}
Utilizare :
interface Data {
id: number,
name: string,
age: number
}
let foo: Foo<Data, 'name' | 'age'>
foo = {
data: [{ id: 1, name: 'Alice', age: 20 }],
titles: {
id: 'Title for id'
}
}
foo = {
data: [{ id: 1, name: 'Alice', age: 20 }],
titles: {
id: 'Title for id',
name: 'Title for name'
// error: Type '{ id: string; name: string; }' is not assignable to type '{ id: string; }'.
}
}
const titles = {
id: 'Title for id',
name: 'Title for name'
}
foo = {
data: [{ id: 1, name: 'Alice', age: 20 }],
titles: titles
// can not catch this since "Excess Property Checks" does not work in assignments
// see: "http://www.typescriptlang.org/docs/handbook/interfaces.html#excess-property-checks
}
Dacă aveți nevoie de excludeProperties ca variabilă mai degrabă decât ca restricție de tip (adică aveți nevoie de valoarea concretă a acesteia pentru a itera peste ea sau pentru a face alte lucruri), o puteți adăuga înapoi astfel:
interface Foo<T, E extends keyof T> {
data: T[],
titles: { [key in Exclude<keyof T, E>]: string },
excludeProperties: E[]
}
Dar nu ți-ar asigura exhaustivitatea excludeProperties.
let foo: Foo<Data, 'name' | 'age'> = {
data: [{ id: 1, name: 'Alice', age: 20 }],
titles: {
id: 'Title for id'
},
excludePropreties: ['name'] // is valid from the type checking perspective though 'age' is missing
}
De fapt, atâta timp cât titles este furnizat folosind literele de obiect, adică verificările excesive de proprietăți își fac efectul, poți să obții toate excludeProperties din titles: const excludeProperties=Object.keys(foo.titles).
Totuși, exhaustivitatea nu este garantată dacă titles este furnizat folosind o altă variabilă.
Explicație pentru motivul pentru care schimbarea excludeProperties în timpul execuției este imposibilă
Presupunem că avem un tip care satisface cerința. Atunci, o variabilă ca aceasta se conformează acestuia:
const foo = {
data: { id: 1, age: 2, name: 'Alice' },
titles: { id: 'ID', age: 'Age' },
excludeProperties: ['name']
}
Atunci dacă modific excludeProperties
foo.excludeProperties.push('age')
Pentru a atinge constrângerea așteptată, tipul lui foo.titles ar trebui să se schimbe la { id: string } de la { id: string, age: string }. Dar, foo.titles are o valoare de { id: 'ID', age: 'Age' } și nu există nicio modalitate ca un sistem de tipuri să schimbe valoarea pe care o deține o variabilă. Aici este locul unde tipul imaginat se prăbușește.
Mai mult, regula nici măcar nu se aplică dacă vrei caracteristici dinamice de la ea.
const foo = {
data: { id: 1, age: 2, name: 'Alice' },
titles: { id: 'ID' },
excludeProperties: ['name', 'age']
}
atunci, dacă alerg
foo.excludeProperties.pop()
Este imposibil ca foo.titles să creeze o valoare pentru cheia age din aer.
Actualizare: un alt mod de a explica imposibilitatea lui 'excludeProperties' dinamic
Compilatorul nu poate ști ce va conține excludeProperties în avans la timpul de compilare, deoarece acesta va fi atribuit la timpul de execuție. Prin urmare, nu știe ce să excludă și ce ar trebui să fie tipul lui titles.
Deci, pentru ca compilatorul să poată determina ce să excludă, trebuie să facem ca excludeProperties să fie definitiv la timpul de compilare. Soluția mea prezentată mai sus realizează acest lucru exprimând această idee de excludere printr-un parametru de tip generic, care este definitiv la timpul de compilare.
Se poate spune că Object.freeze(['id', 'name']) este o altă modalitate de a exprima excluderea definitivă la timpul de compilare. Din cunoștințele mele, nu există nici o modalitate în TypeScript de a exploata această „definitivitate”.
Și instinctul meu îmi spune că este adevărat. Un sistem de tip static trebuie să colecteze toată cunoașterea sa despre cod doar dintr-o analiză statică, adică nu trebuie să înțeleagă și să presupună comportamentul unei anumite funcții (Object.freeze în acest caz). Atâta timp cât sistemul de tipuri este agnostic față de comportamentul real al Object.freeze, așa cum ar trebui să fie, Object.freeze(['id', 'name']) nu este o expresie definitivă la timpul de compilare.
De fapt, esența faptului că nu poți spune compilatorului despre informațiile de tip printr-o variabilă de tip array este că nu există nicio modalitate în JavaScript de a exprima un array imuabil cu un literal (Object.freeze este un apel de funcție, nu un literal), ceea ce, totuși, poate fi exploatat de compilator.
