Typescriptin sisäkkäisten tyyppien union menettää avaimen, jota tyypeillä ei ole yhteistä
Uskon, että tämä saattaa olla samankaltaista kuin Typescript - union-tyypit, ongelma TypeScript-tyypityksen kanssa ja Typescript-ominaisuus ei ole olemassa union-tyypissä, mutta mikään niistä ei ole täysin sama. Kun union-tyyppi luodaan Object.entries...map-menetelmällä, näyttää siltä, että avaimet, joita ei jaeta, poistetaan. Tässä on esimerkkikoodi:
type Params = {
head1:{
sub1:{
a:string
}},
head2:{
sub2:{
a:string,
b:number
}}}
(params:Params)=>{
Object.entries(params).map(([headkey, sub])=>{
Object.entries(sub).map(([subkey, subval])=>{
subval.a // <- Works fine
if('b' in subval)
subval.b // <- Error, "Property 'b' does not exist on type '{ a: string; }'"
})
})
}
Leikkikenttä
Onko mitään keinoa voittaa tämä? Onko tämä virhe?
Vastaus #1
Se ei ole virhe, vaikka sillä on vaikutuksia, joista et pidä tässä tilanteessa. Kun kääntäjän täytyy päätellä yksi tyyppi useasta muusta tyypistä, se yrittää syntetisoida sitä, mitä kutsutaan "parhaaksi yhteiseksi tyypiksi". Usein tämä on suora unioni kaikista mukana olevista tyypeistä:
interface X { x: string }
declare const x: X;
interface Y { y: string; }
declare const y: Y;
const eitherXY = Math.random() < 0.5 ? x : y
// const eitherXY: X | Y
Mutta joskus ainakin yksi osapuolena olevista tyypeistä voidaan osoittaa joksikin toiseksi mukana olevaksi tyypiksi, ja näissä tapauksissa kääntäjä päättelee vain yhden, yleisemmän tyypin unionin sijaan:
interface Z { x: string, z: number }
declare const z: Z;
const eitherXZ = Math.random() < 0.5 ? x : z
// const eitherXZ: X // <-- this is X, not X | Z
Tämä toimii, koska vaikka sitä ei ole julistettu sellaiseksi, Z extends X. TypeScriptin olioiden tyyppejä voidaan laajentaa ominaisuuksien lisäämisen kautta rakenteellisen tyypityksen vuoksi. Ja niin kääntäjä päättää tehdä päättelemisen pelkästään X eikä X | Z.
Nyt maailmassa, jossa TypeScriptiltä löytyisi täysin oikea tyyppijärjestelmä, joka perustuu rakenteelliseen ali-tyyppiin, unionin X | Z ja pelkän X välillä ei ehkä olisi havaittavaa eroa, jos Z extends X. Loppujen lopuksi voisi ajatella tyyppejä vastaavan kaikkien niiden JavaScript-arvojen joukkoa, jotka voidaan sijoittaa kyseiseen tyyppiin. Jos Z extends X, niin jokainen Z-tyyppiä oleva arvo on myös X-tyyppiä oleva arvo, ja näin ollen X | Z ja X vastaavat samaa arvon joukkoa. Tässä maailmassa valinta X | Z ja pelkän X välillä olisi puhtaasti kysymys siitä, miten tyyppi näytetään.
Mutta emme elä siinä maailmassa.
Yksi paikka, jossa X | Z on havaittavasti erilainen kuin X, on objektien literalien ylimääräisten ominaisuuksien tarkistuksen yhteydessä:
const xOrZ: X | Z = { x: "hello", z: 123 }; // okay
const justX: X = { x: "hello", z: 123 }; // error, excess property
Liialliset ominaisuuksia koskevat varoitukset ovat teknisesti väärin positiivisia, jos ajattelet niitä tyyppivirheinä, joten ehkä on parempi ajatella niitä linter-varoituksena sen sijaan.
Toinen paikka on silloin, kun käytät in-operaattoria tyyppisuojana:
declare const x: X;
if ("z" in x) {
x.z.toFixed(); // error
}
declare const xz: X | Z;
if ("z" in xz) {
xz.z.toFixed(); // okay
}
Tämä on tunnetusti teknisesti virheellinen. Loppujen lopuksi arvo {x: "", z: ""} pitäisi olla kelvollinen arvo tyypille X ja siten kelvollinen arvo tyypille X | Z:
const hmm = { x: "", z: "" }
const alsoX: X = hmm; // okay
const alsoXZ: X | Z = hmm; // okay
Joten pitäisi todella olla virhe kuvitella, että pelkkä z-ominaisuuden läsnäolo tarkoittaa, että se on number. Kumpikaan yllä olevista in-tyyppisuojista ei toimisi, jos olisimme tiukkoja. Mutta käytännössä on hyvin hyödyllistä olettaa, että voit käyttää in tyyppisuojana (ja ihmiset yleensä haluavat vielä enemmän; vain xz.z ilman, että tarkistetaan ensin "z" in xz, mutta tähän on jatkuvasti suhtauduttu kielteisesti).
Kaikki tämä tarkoittaa, että riippuen siitä, miten ajattelet rakenteellista alityyppiyhteyttä ja liitoksia, ja vastaako se sitä, miten kääntäjä käsittelee tyyppiä tai lauseketta, saat todennäköisesti oikein mutta ärsyttäviä varoituksia tai virheellisiä mutta hyödyllisiä varoitusten puutteita... kumpikaan ei ole bugi.
Joten täällä ei ole virhettä.
Tuosta puhuen, mitä voisit tehdä sen voittamiseksi? Tapauksessasi ongelma on, että Object.entries()-funktiolla on seuraava kutsuallekirjoitus:
/* (method) ObjectConstructor.entries<T>(o: {
[s: string]: T;
} | ArrayLike<T>): [string, T][] (+1 overload) */
Ja kun kutsut sitä sub-koodilla, kääntäjä päättelee T:ksi {a: string}, joka on paras yleinen ylityyppi {a: string} ja {a: string, b: number} välillä:
Object.entries(sub)
/* (method) ObjectConstructor.entries<{ a: string; }>(...) */
Siksi yksinkertaisin tapa kiertää se on määrittää selvästi T olevan se yhdisteitä tyyppi, jota odotit. Voit joko kirjoittaa sen muodossa {a: string} | {a: string, b: number}, tai voit antaa kääntäjän laskea tämän tyypin Params-tyyppisten arvojen perusteella:
type SubSubParams =
{ [K in keyof Params]: Params[K][keyof Params[K]] }[keyof Params]
/* type SubSubParams = {
a: string;
} | {
a: string;
b: number;
} */
ja sitten
Object.entries<SubSubParams>(sub).map(([subkey, subval]) => {
subval.a
if ('b' in subval)
subval.b // okay
})
Leikkikenttälinkki koodiin
