Ikke-diskjunktiv union av et vilkårlig antall typer i TypeScript
I TypeScript ønsker jeg å lage en unions-type som representerer verdier som tilhører én eller flere forskjellige typer, lignende oneOf i OpenAPI eller JSON Schema. Ifølge et tidligere svar på et lignende spørsmål, skal union-operatoren i TypeScript gjøre dette, da den representerer mengdeunion (inkluderende eller). Dette samsvarer imidlertid ikke med typinferens-atferden i TypeScript når man bruker in-operatoren som en typebeskyttelse, som ser ut til å anta at verdier tilhører akkurat én av de unionerte typene (diskjunktiv union eller eksklusivt eller). Som et enkelt eksempel fører dette til en typefeil:
interface SoftwareDeveloper { code(): void; }
interface Chef { cook(): void; }
type SoftwareDeveloperOrChef = SoftwareDeveloper | Chef;
// Determined at runtime. May have the code method, the cook method, or both.
const person = { code: () => { }, cook: () => { } };
const softwareDeveloperOrChef: SoftwareDeveloperOrChef = person;
if ("code" in softwareDeveloperOrChef) {
// softwareDeveloperOrChef has inferred type SoftwareDeveloper
softwareDeveloperOrChef.code();
// Coding is hungry work
if ("cook" in softwareDeveloperOrChef) {
// Not allowed, since softwareDeveloperOrChef has inferred type never
softwareDeveloperOrChef.cook();
}
}
For å få den oppførselen jeg ønsker og unngå typefeil, må jeg manuelt legge til alle kombinasjoner av snitttyper til unionen:
type SoftwareDeveloperOrChefOrBoth = SoftwareDeveloper | Chef | (SoftwareDeveloper & Chef);
const softwareDeveloperOrChefOrBoth: SoftwareDeveloperOrChefOrBoth = person;
if ("code" in softwareDeveloperOrChefOrBoth) {
// softwareDeveloperOrChef has inferred type SoftwareDeveloper | (SoftwareDeveloper & Chef)
softwareDeveloperOrChefOrBoth.code();
if ("cook" in softwareDeveloperOrChefOrBoth) {
// Allowed, since softwareDeveloperOrChefOrBoth has inferred type SoftwareDeveloper & Chef
softwareDeveloperOrChefOrBoth.cook();
}
}
Et mellomliggende spørsmål kan være om dette er oppførselen jeg bør forvente? Men gitt at dette er den implementerte oppførselen, er jeg egentlig mer interessert i en måte å konstruere den ikke-diskjunktive unionstypen for et vilkårlig antall typer. Å gjøre det manuelt fører til en eksponentielt økende størrelse på definisjonen av unionstypen ettersom antallet typer øker:
type AorB = A | B | A & B;
type AorBorC = A | B | C | A & B | A & C | B & C | A & B & C;
type AOrBOrCorD = A | B | C | D | ... | B & C & D | A & B & C & D;
Jeg kan kode generiske typer for et spesifikt antall parametere:
type AnyOf2<A, B> = A | B | A & B;
type AnyOf3<A, B, C> = AnyOf2<A, AnyOf2<B, C>>;
type AnyOf4<A, B, C, D> = ...;
Men kan jeg lage en lignende generisk type som tar et vilkårlig antall typer, for eksempel som en union (AnyOf<A | B>) ved å bruke teknikker som de som brukes i UnionToIntersection-verktøystypen implementert her?
Svar #1
Grunnen til at du får feil er at bruk av en unionstype ikke riktig tillater at SoftwareDeveloperOrChef kan være både SoftwareDeveloper og Chef. Hvis vi vet at objektet er en SoftwareDeveloper, har vi ingen informasjon i det hele tatt om cook-egenskapen -- den kan være hva som helst. Den trenger ikke å være en funksjon av forventet type.
Du er nesten der med din type AnyOf2<A, B>=A | B | A & B;, men når vi jobber med egenskaper på et objekt, kan vi bruke Partial og Required for å få en bedre type.
Den endelige typen vi håper å oppnå for SoftwareDeveloperOrChefOrBoth er en hvor cook og code begge er valgfrie funksjoner, men minst én er påkrevd.
Prøv dette:
type EitherOrBoth<A, B> = Partial<A & B> & (A | B);
type SoftwareDeveloperOrChef = EitherOrBoth<SoftwareDeveloper, Chef>;
En endring er at ved å bruke Partial kan du ikke lenger bruke "code" in softwareDeveloperOrChef som en typevakt fordi objektet kan ha egenskapsnøkkelen eksplisitt satt til undefined. For eksempel er {code: undefined; cook:() => {};} et gyldig medlem av vår type.
Men denne koden sjekker ut
const softwareDeveloperOrChef: SoftwareDeveloperOrChef = person;
if (softwareDeveloperOrChef.code) {
softwareDeveloperOrChef.code();
if (softwareDeveloperOrChef.cook) {
// this is allowed now
softwareDeveloperOrChef.cook();
}
}
Rediger
I henhold til vår diskusjon i kommentarfeltet, ønsker du at de individuelle grensesnittene SoftwareDeveloper og Chef skal ha flere metoder, og du ønsker å kunne vite med sikkerhet at hvis en metode av Chef er til stede, så vil alle de andre metodene til Chef også være til stede.
For maksimal typesikkerhet bør den kombinerte SoftwareDeveloperOrChefOrBoth-typen eksplisitt angi at alle medlemmer enten må ha alle metoder fra Chef eller ingen.
Denne nyttige typen Not<T> håndterer dette ved å si at alle nøkler i T ikke kan settes til noe annet enn undefined:
export type Not<T> = {
[P in keyof T]?: never;
};
Merk at det å ikke inkludere egenskapen ikke sier at den ikke kan settes. Som forklart i den lenkede tråden, med din type AorB ville du få en feil når du oppretter et nytt ugyldig objekt (et som har alle metodene til SoftwareDeveloper og noen, men ikke alle, metodene til Chef), men ikke når du tilordner typen til et eksisterende objekt fordi dette ugyldige objektet kan tilordnes A og derfor også kan tilordnes AorB.
I stedet for den løsere EitherOrBoth<A,B>-typen som ble foreslått tidligere, her er en strengere type som håndhever eksklusivitet:
type ExclusiveAndOr<A, B> = A & Not<B> | B & Not<A> | A & B;
type SoftwareDeveloperOrChef = ExclusiveAndOr<SoftwareDeveloper, Chef>;
Med denne typen er det ikke lenger bare en antagelse at hvis en person kan lage mat, kan de også hakke.
function checkPerson( softwareDeveloperOrChef: SoftwareDeveloperOrChef ) {
if (softwareDeveloperOrChef.code) {
softwareDeveloperOrChef.code();
if (softwareDeveloperOrChef.cook) {
// should be ok
softwareDeveloperOrChef.cook();
// ok to assume all methods exist
softwareDeveloperOrChef.chop();
} else {
// should be error
softwareDeveloperOrChef.chop();
}
}
}
Typescript-lekeplasslenke
