typescript - Union og snitt av typer


For å øve skrev jeg denne koden:

type Cat = {name: string, purrs: boolean};
type Dog = {name: string, barks: boolean, bites: boolean};

type CatOrDogOrBoth = Cat | Dog;
type CatAndDog = Cat & Dog;

let pet1 : CatOrDogOrBoth = {name: "pooky", purrs: true, bites: false};
let pet2 : CatAndDog = {name: "Timmy" };

Men TypeScript-kompilator gir feil på pet2 og sier at jeg må legge til purrs i objektet mitt. Men er ikke CatAndDog krysningspunktet av de to typene, så bare en name-egenskap?

Svar #1

Du har blandet sammen skjæringspunkt (intersection) og union, noe som av en eller annen grunn ikke er uvanlig når folk lærer om TypeScript. En sannsynlig årsak til denne forvirringen er at en objekttype er kontravariante i typen til nøklene sine, så et skjæringspunkt av objekttyper har en union av nøklene sine og omvendt. Det vil si, keyof (Cat & Dog) er det samme som (keyof Cat) | (keyof Dog), og keyof (Cat | Dog) er det samme som (keyof Cat) & (keyof Dog):

type KeyExploration = {
  keysOfIntersection: keyof (Cat & Dog) // "name" | "purrs" | "barks" | "bites"
  unionOfKeys: keyof Cat | keyof Dog // "name" | "purrs" | "barks" | "bites"
  keysOfUnion: keyof (Cat | Dog) // "name"
  intersectionOfKeys: (keyof Cat) & (keyof Dog) // "name"
}

På grunn av denne kontravariansen vil du blande sammen snitt og unioner hvis din oppfatning av en objekttype er ekvivalent med dens sett av deklarerte egenskaper. I stedet bør du tenke på en type som et sett av tillatte verdier (se dette svaret for mer informasjon). Og du bør tenke på unioner og snitt av typer som unioner og snitt av settene av verdier som kan tildeles disse typene.

For en bokstavelig type som "foo", har settet ett element: { "foo" }. For en type som string, er det (praktisk talt) det uendelige settet av alle JavaScript-strenger: { x | typeof x === "string" } (ved bruk av mengdekonstruksjonsnotasjon). Og for objekttyper som {y: 0}, er det også et uendelig sett: { x | x.y === 0 }... det vil si, settet av alle JavaScript-objekter med en y-egenskap som er nøyaktig lik 0.


En annen mulig kilde til forvirring er at objekttyper i TypeScript er åpne og ikke lukkede eller eksakte (se microsoft/TypeScript#12936 for en forespørsel om eksakte typer).

Objekttypedefinisjoner viser hvilke egenskaper må være til stede, men de sier ikke noe om hvilke egenskaper ikke må være til stede. Et objekt kan ha flere egenskaper enn det som er nevnt i typens definisjon:

interface Garfield extends Cat {
  hatesMondays: true,
  eatsLasagna: true,
}
declare const garfield: Garfield;
const garfieldAsACat: Cat = garfield; // okay

(Dette kompliseres litt av tilstedeværelsen av sjekker for overflødige egenskaper, som behandler objektliteraler som om de var eksakte typer. Men slike sjekker er unntaket og ikke regelen.)

Siden objekttyper er åpne, betyr det at settet av tildelbare verdier er større enn du kanskje trodde. To objekttyper som {a: 0} og {b: 1} har faktisk betydelig overlapp; for eksempel er verdien {a: 0, b: 1, c: 2, d: 3} tildelbar til begge.


La oss nå tenke på snitt (&) og forening (|):

Hvis jeg har et objekt av typen Cat & Dog, må det kunne tildeles til begge Cat og Dog. Fordi objekttyper er åpne, sier ingenting at en Cat ikke kan ha en barks eller en bites-egenskap. Og ingenting sier at en Dog ikke kan ha en purrs-egenskap. Så hvis du har noe som er både en Cat og en Dog, må det ha alle egenskapene til begge typer.

let okay1: CatAndDog = 
  { name: "CatDog", purrs: true, bites: true, barks: true }; // Cat and Dog

Og pet2 mislykkes fordi det verken er en Cat eller en Dog:

let pet2: CatAndDog = { name: "Timmy" }; // neither Cat nor Dog

På den annen side må et objekt av typen Cat | Dog bare kunne tilordnes enten Cat eller Dog. Hvis du tilordner en verdi til en variabel av typen Cat | Dog, må det være minst én av disse:

let okay1: CatOrDogOrBoth = 
  { name: "Sylvester", purrs: false }; // Cat
let okay2: CatOrDogOrBoth = 
  { name: "Odie", barks: true, bites: false }; // Dog

Din pet1 er akseptabel fordi det er en Cat. Den har en ekstra bites-egenskap, noe som er greit (og blir ikke fanget opp av sjekking av overflødige egenskaper, selv om noen mener det burde være det (se microsoft/TypeScript#20863):

let pet1: CatOrDogOrBoth = 
  { name: "pooky", purrs: true, bites: false }; // Cat with bites:false

Hvis jeg har et objekt av typen Cat | Dog og jeg ennå ikke har inspisert det for å se hvilken av Cat eller Dog det er, er den eneste sikre egenskapen jeg kan få tilgang til, dens name, fordi det er den eneste egenskapen jeg vet med sikkerhet vil være til stede. Det er mulig at en Cat | Dog vil ha noen egenskaper fra begge typer, som du viser ved initialiseringen av pet1, men du kan ikke garantere det.


Lenke til kode

Relaterte artikler