TypeScript härleder en never-typ när en typ-predikatfunktion returnerar false


Jag använder en funktion som returnerar ett typ-predikat för att tala om för TypeScript att en källa definitivt har eller inte har en media-egenskap. Om det är en SourceWithoutMedia bör det hanteras på ett sätt, och om det är en SourceWithMedia bör det hanteras på ett annat sätt.

Problemet är vad TypeScript härleder från det fall där typpredikatet returnerar false.

Jag skulle förvänta mig att TypeScript drar slutsatsen att om isSourceWithoutMedia(source) returnerar false, så är source en SourceWithMedia.

Vad det faktiskt antyder är att om isSourceWithoutMedia(source) returnerar false, då är typen av source never.

Vad missar jag här, och finns det ett sätt att få TypeScript att härleda att om isSourceWithoutMedia(source) returnerar false, så är source en SourceWithMedia?

interface SourceWithMedia {
    id: string
    someOtherProperty: string
    media?: string
}

interface SourceWithoutMedia {
    id: string
}

const isSourceWithoutMedia = (source: SourceWithMedia | SourceWithoutMedia): source is SourceWithoutMedia => {
    return Object.keys(source).length === 1
}

function doSomethingWithMedia (source: SourceWithMedia | SourceWithoutMedia) {
    if (isSourceWithoutMedia(source)) return source
    
    if (source.media) return source.media // Error: Property 'media' does not exist on type 'never'.
    
}

Exempel på TypeScript-lekplats

Svar #1

...om isSourceWithoutMedia(source) returnerar falskt, då är source en SourceWithMedia?

Nej.

Problemet är att SourceWithMedia utökar SourceWithoutMedia, det vill säga SourceWithMedia är en subtyp av SourceWithoutMedia.

const withMedia: SourceWithMedia = {id: "", someOtherProperty: "", media: ""}
const withoutMedia = withMedia; // valid

När man försöker begränsa source till SourceWithoutMedia med det där villkoret händer egentligen ingenting. source kommer fortfarande att ha en unionstyp, även om du använder något som !("media" in source) i ditt villkor eller först kontrollerar efter en saknad media-parameter på följande sätt:

function doSomethingWithMedia(source: SourceWithMedia | SourceWithoutMedia) {
  if (isSourceWithoutMedia(source)) {
    source;
    //^? (parameter) source: SourceWithMedia | SourceWithoutMedia
  }
}

Nu, om vi följer den negativa grenen av if-satsen finns det inga medlemmar i unionen kvar att begränsa. TypeScript representerar tomma unioner med typen never.


Å andra sidan, om du inverterar din predikatlogik och kontrollerar om media-egenskapen finns där uttryckligen, kommer du att kunna begränsa source till SourceWithMedia. På så sätt kan TypeScript faktiskt skilja mellan de två typerna och ange att vi har att göra med ett värde av typen SourceWithMedia eftersom SourceWithoutMedia inte kan tilldelas till SourceWithMedia.

TypeScript-lekplats

Svar #2

TypeScript förstår inte relationen mellan det första villkorsblocket här och det andra.

En lösning på detta skulle vara att explicit typomvandla källan till SourceWithMedia.

function doSomethingWithMedia (source: SourceWithMedia | SourceWithoutMedia) {
  if (isSourceWithoutMedia(source)) return source

  const sourceWithMedia = source as SourceWithMedia
  if (sourceWithMedia.media) return sourceWithMedia.media;
}