typescript - Typavsmalningsbeteende fungerar inte beroende på vilka typer som är inblandade


Jag har lite kod för typavsmalning som orsakar ett kompileringsfel. När jag gör till synes små ändringar i de typer som är involverade i avsmalningen, går kompilatorns kontroller igenom. Typerna överlappar delvis och involverar valfria attribut.

Finns det några regler kring typförminskning som kan förklara detta beteende?

När kompileringen misslyckas verkar inte den typ som borde ha smalnats vara smalnad — i det här fallet är det Pet. När du gör en ändring av typerna som kompilerar framgångsrikt, är typen Pet & Dog.

Jag stötte på detta problem med produktionskod, men har skapat en enkel reproduktion.

Jag kollade tidigare TypeScript-versioner på playgrounden och beteendet verkar vara detsamma, så jag tror inte att detta är ett nytt kompilatorbeteende.

interface Pet {
  name: string; // remove this or make it nullable and it compiles.
  isFriendly: boolean; // remove this it compiles.
}

interface Dog {
  name: string; // remove this and it compiles
  ownerName?: string; // make this non-nullable and it compiles
  // isHappy: boolean; // Add any other non-nullable property and it compiles
}

function isDog(value: any): value is Dog {
  return true;
}

// change the type here to Pet | Dog, or Pet & Dog and it compiles
const testAnimal: Pet = {} as any;

if (isDog(testAnimal)) {
  // when the compiler error occurs, the type of `testAnimal` is `Pet` here, not `Dog` and not `Pet & Dog`
  console.log(testAnimal.ownerName); // <-- Compiler error here: Property 'ownerName' does not exist on type 'Pet'
}

Här är en länk till samma kod på Typescript Playground

Svar #1

Användardefinierade typvaktsfunktioner kan endast ha predikatreturtyper som smalnar av — de kan inte utvidga eller på annat sätt godtyckligt ändra typen på operandparametern.

På grund av det behöver predikatet för din typvakt vara en snittning av indatatypen och Dog, så:

declare function isDog<T>(value: T): value is T & Dog;

Att använda en generisk typparameter för value är lämpligt här för att bevara typinformationen utan att behöva känna till den i förväg (kompilatorn kommer att härleda den) — du kan till och med begränsa den med en annan, mer allmän typ (beroende på ditt användningsfall — till och med något som object är bättre än inget).

Här är ett komplett exempel med de data du tillhandahöll:

TS Lekplats

function hasOptionalProperty<
  O extends object,
  K extends PropertyKey,
  V,
>(
  obj: O,
  prop: K,
  valueValidatorFn: (value: unknown) => value is V,
): obj is O & Partial<Record<K, V>> {
  return (
    !(prop in obj) ||
    valueValidatorFn((obj as Record<K, unknown>)[prop])
  );
}

interface Pet {
  name: string;
  isFriendly: boolean;
}

interface Dog {
  name: string;
  ownerName?: string;
}

function isDog<T>(value: T): value is T & Dog {
  return (
    typeof value === "object" && value !== null &&
    "name" in value && typeof value.name === "string" &&
    hasOptionalProperty(
      value,
      "ownerName",
      (value): value is string => typeof value === "string",
    )
  );
}

const testAnimal: Pet = { name: "Ani", isFriendly: true };

if (isDog(testAnimal)) {
  console.log(testAnimal.ownerName); // Ok
            //^? const testAnimal: Pet & Dog
}