typescript - Finnes det et alternativ til Partial for å bare akseptere felter fra en annen type og ingenting annet?
Gitt grensesnitt eller klasser A og B med et x1-felt til felles
interface A {
a1: number;
x1: number; // <<<<
}
interface B{
b1: number;
x1: number; // <<<<
}
Og gitt implementeringene a og b
let a: A = {a1: 1, x1: 1};
let b: B = {b1: 1, x1: 1};
Typescript tillater dette, selv om b1 ikke er en del av A:
let partialA: Partial<A> = b;
Du kan finne forklaringen på hvorfor dette skjer her: Hvorfor godtar Partial ekstra egenskaper fra en annen type?
Er det et alternativ til Partial for å kun akseptere felter fra en annen type og ingenting annet (uten å kreve alle feltene)? Noe sånt som en StrictPartial?
Dette har forårsaket mange problemer i kodebasen min, da det rett og slett ikke oppdager at feil klasse blir sendt som parametere til funksjonene.
Svar #1
Det du egentlig ønsker, kalles eksakte typer, hvor noe som "Exact<Partial<A>>" ville forhindre overflødige egenskaper i alle tilfeller. Men TypeScript støtter ikke direkte eksakte typer (i det minste ikke fra TS3.5), så det finnes ingen god måte å representere Exact<> som en konkret type. Du kan simulere eksakte typer som en generisk begrensning, noe som betyr at plutselig må alt som håndterer dem bli generisk i stedet for konkret.
Den eneste gangen der typesystemet behandler typer som eksakte er når det gjør overskytende egenskapssjekker på "ferske objektnoter", men det finnes noen kanttilfeller der dette ikke skjer. Et av disse kanttilfellene er når typen din er svak (ingen obligatoriske egenskaper) som Partial<A>, så vi kan ikke stole på overskytende egenskapssjekker i det hele tatt.
Og i en kommentar sa du at du vil ha en klasse hvis konstruktør tar et argument av typen Exact<Partial<A>>. Noe som dette
class Example {
constructor(public partialA: Exact<Partial<A>>) {} // doesn't compile
}
Jeg skal vise deg hvordan du får noe sånt, sammen med noen forbehold underveis.
La oss definere den generiske typealiasen
type Exactly<T, U> = T & Record<Exclude<keyof U, keyof T>, never>;
Dette tar en type T og en kandidat-type U som vi ønsker å sikre er «nøyaktig T». Det returnerer en ny type som ligner på T men med ekstra never-verdier for egenskaper som tilsvarer de ekstra egenskapene i U. Hvis vi bruker dette som en begrensning på U, som U extends Exactly<T, U>, kan vi garantere at U matcher T og ikke har noen ekstra egenskaper.
For eksempel, forestill deg at T er {a: string} og U er {a: string, b: number}. Da blir Exactly<T, U> tilsvarende {a: string, b: never}. Legg merke til at U extends Exactly<T, U> er feil, siden deres b-egenskaper er inkompatible. Den eneste måten U extends Exactly<T, U> er sann på er hvis U extends T men ikke har noen ekstra egenskaper.
Så vi trenger en generisk konstruktør, noe sånt som
class Example {
partialA: Partial<A>;
constructor<T extends Exactly<Partial<A>, T>>(partialA: T) { // doesn't compile
this.partialA = partialA;
}
}
Men du kan ikke gjøre det fordi konstruktørfunksjoner ikke kan ha sine egne typeparametere inne i klasseerklæringer. Dette er en uheldig konsekvens av samspillet mellom generiske klasser og generiske funksjoner, så vi må finne en løsning på det.
Her er tre måter å gjøre det på.
1: Gjør klassen "unødvendig generisk". Dette gjør konstruktøren generisk som ønsket, men får de konkrete forekomstene av denne klassen til å bære med seg en spesifisert generisk parameter:
class UnnecessarilyGeneric<T extends Exactly<Partial<A>, T>> {
partialA: Partial<A>;
constructor(partialA: T) {
this.partialA = partialA;
}
}
const gGood = new UnnecessarilyGeneric(a); // okay, but "UnnecessarilyGeneric<A>"
const gBad = new UnnecessarilyGeneric(b); // error!
// B is not assignable to {b1: never}
2: Skjul konstruktøren og bruk en statisk funksjon i stedet for for å lage forekomster. Denne statiske funksjonen kan være generisk mens klassen ikke er:
class ConcreteButPrivateConstructor {
private constructor(public partialA: Partial<A>) {}
public static make<T extends Exactly<Partial<A>, T>>(partialA: T) {
return new ConcreteButPrivateConstructor(partialA);
}
}
const cGood = ConcreteButPrivateConstructor.make(a); // okay
const cBad = ConcreteButPrivateConstructor.make(b); // error!
// B is not assignable to {b1: never}
3: Lag klassen uten den eksakte begrensningen, og gi den et fiktivt navn. Bruk deretter en typepåstand for å lage en ny klassekonstruktør fra den gamle som har den generiske konstruktørsignaturen du ønsker:
class _ConcreteClassThatGetsRenamedAndAsserted {
constructor(public partialA: Partial<A>) {}
}
interface ConcreteRenamed extends _ConcreteClassThatGetsRenamedAndAsserted {}
const ConcreteRenamed = _ConcreteClassThatGetsRenamedAndAsserted as new <
T extends Exactly<Partial<A>, T>
>(
partialA: T
) => ConcreteRenamed;
const rGood = new ConcreteRenamed(a); // okay
const rBad = new ConcreteRenamed(b); // error!
// B is not assignable to {b1: never}
Alle disse bør fungere for å akseptere "eksakte" Partial<A> instanser og avvise ting med ekstra egenskaper. Vel, nesten.
De avviser parametere med kjente ekstra egenskaper. Typesystemet har egentlig ikke en god representasjon for eksakte typer, så enhver objekt kan ha ekstra egenskaper som kompilatoren ikke kjenner til. Dette er essensen av erstattbarheten til underklasser for superklasser. Hvis jeg kan gjøre class X {x: string} og deretter class Y extends X {y: string}, så er hver instans av Y også en instans av X, selv om X ikke vet noe om y-egenskapen.
Så du kan alltid utvide en objekttype for å få kompilatoren til å glemme egenskaper, og det er gyldig: (Overflødig egenskapssjekking har en tendens til å gjøre dette vanskeligere i noen tilfeller, men ikke her)
const smuggledOut: Partial<A> = b; // no error
Vi vet at det kompilerer, og ingenting jeg gjør kan endre det. Og det betyr at selv med implementasjonene ovenfor, kan du fortsatt sende inn en B:
const oops = new ConcreteRenamed(smuggledOut); // accepted
Den eneste måten å forhindre det på er med en slags kjøretidssjekk (ved å undersøke Object.keys(smuggledOut)). Så det er en god idé å bygge en slik sjekk inn i klassekonstruktøren din hvis det virkelig er skadelig å akseptere noe med ekstra egenskaper. Eller, du kan bygge klassen din på en slik måte at den stille forkaster ekstra egenskaper uten å ta skade av dem. Uansett er de ovennevnte klassedefinisjonene omtrent så langt typesystemet kan skyves i retning av eksakte typer, i hvert fall for nå.
Håper det hjelper; lykke til!
Lenke til kode
