typescript - Forbedre overordnet objekt ved hjelp av typevakt på kun en egenskap
Jeg har en type Box<T> hvor T alltid vil være noe som utvider en State som er en diskriminert union.
type StateA = { type: "A"; a: number };
type StateB = { type: "B"; b: string };
type State = StateA | StateB;
interface Box<T extends State = State> {
state: T;
}
Jeg vil nå gjerne inspisere hva slags Box en variabel er og avgrense typeargumentet til den mer spesifikke typen. Den naive tilnærmingen med å sjekke typen til det diskriminerte unionet fungerer på en måte. Den vanlige typeinferensen ved bruk av if eller switch-setninger fungerer fint når du umiddelbart får tilgang til egenskapene til den boksete tilstanden. Men typen til Box avgrenses ikke, bare typen til Box.state.
const b: Box = { state: { type: "B", b: "str" } };
if (b.state.type === "B") {
console.log(b.state.b); // Inferred correctly
const bRefined: Box<StateB> = b; // Assignment not possible
}
Dette kan imidlertid omgås med en brukerdefinert typevakt:
function isBoxB(b: Box): b is Box<StateB> {
return b.state.type === "B";
}
if (isBoxB(b)) {
console.log(b.state.b); // Inferred correctly
const bRefined: Box<StateB> = b; // Assignment possible
}
Jeg kan leve med den løsningen, men jeg er ikke akkurat fornøyd med den. Finnes det en bedre måte å automatisk begrense typen til den omkringliggende Box uten å skrive egendefinerte typevakter?
Hele koden er tilgjengelig på Typescript Playground.
Svar #1
I TypeScript begrenser innsnevring av den tilsynelatende typen til en objekts egenskap (eller under-egenskap, eller under-under-egenskap, osv.) vanligvis ikke den tilsynelatende typen til selve objektet. Den eneste gangen dette skjer er hvis objektet er av en diskriminert unionstype og egenskapen du sjekker er dens diskriminant.
I ditt tilfelle, selv om state-egenskapen til Box er av den diskriminerte unionstypen State, er ikke Box selv en diskriminert union. Den er ikke en union i det hele tatt. Så hvis du har en verdi b av typen Box, vil det selv om en sjekk av b.state.type vil snevre inn b.state, ikke snevre inn b selv.
Dette er en kjent begrensning i TypeScript, og har blitt rapportert flere ganger. Det nåværende åpne problemet som ber om forbedring her er microsoft/TypeScript#42384. Tidligere ble disse bare lukket som for kostbare å fikse (for at en sjekk av a.b.c.d.e skulle innsnevres til mer enn bare a.b.c.d.e, måtte kompilatoren kanskje syntetisere nye typer for å representere effektene på a.b.c.d, a.b.c, a.b og a). En kommentar antyder at situasjonen kanskje har utviklet seg og at det er mulig å implementere. Men foreløpig er det ikke implementert.
Inntil og med mindre dette endrer seg, må vi bare jobbe rundt det.
En løsning som noen ganger fungerer for folk, er å kopiere det eksisterende objektet til et nytt objekt hvor den sjekkede egenskapen blir utpekt for eksplisitt kopiering. Dette leder kompilatoren gjennom logikken for å syntetisere den snevrede typen. I ditt tilfelle ser det slik ut:
if (b.state.type === "B") {
const bRefined: Box<StateB> = { ...b, state: b.state } // okay
}
Her har vi spredd det eksisterende b-objektet inn i et nytt objektliteral, og deretter eksplisitt kopiert state-egenskapen over. Nå ser kompilatoren at {...b, state: b.state} er av typen Box<StateB>.
Ellers er den generelle løsningen å lage en tilpasset typebeskyttelsesfunksjon som innkapsler konseptet «en sjekk av en egenskap bør avgrense forelderen». Den kan se slik ut:
function hasPropType<T extends object, K extends keyof T, V extends T[K]>(
obj: T, key: K, valGuard: (x: T[K]) => x is V): obj is T & Record<K, V> {
return valGuard(obj[key]);
}
Dette forteller kompilatoren at hvis valGard(obj[key]) er true, så kan hasPropType(obj, key, valGuard) innsnevre typen til obj. I eksemplet ditt kan det se slik ut:
if (hasPropType(b, "state", (x): x is StateB => x.type === "B")) {
const bRefined: Box<StateB> = b; // okay
}
Selvfølgelig, for en enkelt bruk er dette mer komplisert enn bare å bruke metoden din isBoxB(). Men hvis du oppdager at du gjør slike sjekker ofte, trenger du kanskje ikke å bygge individuelle typebeskyttelsesfunksjoner for hver sjekk.
Et kompromiss her kan være å skrive en litt mer generell typebeskytter enn isBoxB, men mindre generell enn hasPropType:
function isBox<K extends State['type']>(type: K, b: Box
): b is Box<Extract<State, { type: K }>> {
return b.state.type === type;
}
if (isBox("B", b)) {
const bRefined: Box<StateB> = b; // okay
}
Lenke til kode på lekeplassen
