Egenskapen AAA finns inte på typen BBB-fel från tsc (typescript)


Jag är ganska nyfiken på varför const { liveCount }=customer inte fungerar nedan. tsc ger Property 'liveCount' does not exist on type 'Customer'.(2339)-felet. Vad är skillnaden mellan dem och vad behöver jag lära mig mer för att förstå detta?

TS 4.2.3 används.

type Thing = {
    name: string
}

type Person = Thing & {
    age: number
    address?: string
}

type Cat = Thing & {
    liveCount: number
}

type Customer = Person | Cat

function deliverTo(customer: Customer) {
    // Case A: OK
    const { liveCount } = { ...customer }

    // Case B: Not OK
    // const { liveCount } = customer
}

Svar #1

Du får endast komma åt egenskaper som finns på alla medlemmar i en union.

Följande beteende är som förväntat:

declare const customer: Customer;
const livecount = customer.liveCount; // error!
// Property 'liveCount' does not exist on type 'Customer'
//  Property 'liveCount' does not exist on type 'Person'.

TypeScript kommer endast att tillåta dig att komma åt en egenskapsnyckel på ett värde av en unionstyp om den egenskapsnyckeln är känd för att finnas i varje medlem av unionen. Se "Working with union types" i TS Handbook. I exemplet ovan kan du inte komma åt customer.liveCount eftersom ingen egenskapsnyckel med namnet "liveCount" är känd för att finnas på ett värde av typen Person.

Så för all kod i ditt exempel bör vi förvänta oss ett fel som säger att liveCount inte finns på Customer. Det faktum att din "Case A", const { liveCount }={ ...customer } råkar inte resultera i ett fel, är uppenbarligen en bugg i TypeScript. Se microsoft/TypeScript#43174, som jag skapade om just denna fråga. Åtminstone från och med idag (10 mars 2021) är den buggen planerad att åtgärdas i TypeScript 4.3.1, så jag skulle inte bli förvånad om du inom snar framtid ser samma fel i både "Case A" och "Case B".


Om du tänker "men vänta, är inte customer.liveCount av typen number | undefined?", är det förmodligen för att din tankegång har gått till så här:

  • customer är antingen en Cat eller en Person. ✔
  • Om customer är en Cat, då kommer customer.liveCount att vara ett nummer. ✔
  • Om customer är en Person, kommer customer inte ha någon liveCount-egenskap alls. ❌
  • Om customer inte har en liveCount-egenskap, kommer customer.liveCount att vara undefined. ✔
  • Så customer.liveCount kommer antingen att vara number eller undefined. ❌
  • Så customer.liveCount har typen number | undefined. ❌

De påståenden som är markerade med kryssrutor (✔) är korrekta, men de som är markerade med korsrutor (❌) är felaktiga för TypeScript. Låt oss fokusera på var det först går fel:

  • Om customer är en Person, kommer customer inte ha någon liveCount-egenskap alls. ❌

Detta är inte sant. Objekt typer i TypeScript är öppna eller utbyggbara. Du kan lägga till egenskaper till en objekttyp utan att bryta den ursprungliga typen. Detta är en bra sak eftersom det möjliggör gränssnitts- och klassutvidgningar för att bilda typ-hierarkier. Om bar är av typen Bar, och Bar extends Foo, då är bar också av typen Foo. Men det har också vissa irriterande konsekvenser, som vi ser här.

Ett värde av typen Person är känt för att ha: en egenskap som heter name vars typ är string; en egenskap som heter age vars typ är number; och eventuellt en egenskap som heter address som, om den finns, har typ string | undefined. Men ett värde av typen Person är inte känt för att sakna några andra egenskaper. Specifikt är det inte känt för att sakna en egenskap som heter liveCount. Och det finns ingen begränsning för vilken typ av egenskap som kan finnas vid den nyckeln. Det kan vara number, eller string, eller Date, eller vad som helst. Det enda säkra att säga är att det är något som unknown eller any. Så vi måste ändra vår logik till något som detta:

  • customer är antingen en Cat eller en Person. ✔
  • Om customer är en Cat, då kommer customer.liveCount att vara ett nummer. ✔
  • Om customer är en Person, kan customer ha en liveCount-egenskap vars typ kan vara vad som helst. ✔
  • Så då kommer customer.liveCount att vara any. ✔
  • Så customer.liveCount kommer antingen att vara number eller any. ✔
  • Så customer.liveCount har typen any, och det är troligen ett misstag att i första hand komma åt liveCount-egenskapen på customer. ✔

Ett exempel:

const undeadCount = {
  name: "Dracula",
  age: 1000,
  liveCount: false
}
const weirdPerson: Person = undeadCount;
deliverTo(undeadCount);

Här är undeadCount en giltig Person eftersom den har ett name och en age av rätt typ. Den har en liveCount-egenskap av typen boolean, men detta diskvalificerar den inte från att vara en Person, vilket visas av att jag kan tilldela den till en variabel weirdPerson av typen Person utan fel. Och jag kan också kalla deliverTo(undeadCount). Om implementeringen av deliverTo() förväntar sig att customer.liveCount ska vara ett number om det inte är undefined, kan några väldigt konstiga saker hända. Du kan till och med få körtidsfel (t.ex. customer.liveCount?.toFixed(2)).


Observera att saker skulle vara annorlunda om du hade ett sätt att säga "en Customer är antingen exakt en Cat eller exakt en Person, där det i inget av fallen skulle kunna finnas extra egenskaper som jag inte känner till". Men TypeScript har för närvarande inte stöd för så kallade "exakta typer", och det finns en långvarig förfrågan om att implementera dem på microsoft/TypeScript#12936. Så, i avsaknad av ett rimligt sätt att säga Exact<Cat> | Exact<Person>, vad kan göras?


Hur man går vidare istället

Det finns olika lösningar. En sak du kan göra är helt enkelt att försäkra att din customer har en valfri liveCount-egenskap som antingen är number eller undefined. Visst, det är tekniskt möjligt att någon skulle kunna skicka in ett undeadCount, men du förväntar dig verkligen inte att det ska hända och du vill inte slösa din tid på att oroa dig för en sådan osannolik händelse. Ett sådant försäkrande är detta:

const { liveCount } = customer as Partial<Cat>; // okay

Här säger vi att customer antingen kommer att ha egenskaperna från Cat eller så kommer de att saknas och därför vara undefined. Detta inkluderar liveCount.

En mer utvecklad version av denna lösning är att säga att customer är en ExclusiveCustomer definierad så här:

type ExclusiveCustomer = {
    name: string;
    age: number;
    address?: string | undefined;
    liveCount?: undefined;
} | {
    name: string;
    liveCount: number;
    age?: undefined;
    address?: undefined;
};

Du säger uttryckligen att om customer är en Person så saknar den alla Cat-specifika egenskaper, och vice versa. Du kan till och med få kompilatorn att beräkna ExclusiveCustomer från typen Customer via en typfunktion som ExclusiveUnion, från denna andra SO-fråga:

const { liveCount } = customer as ExclusiveUnion<Customer>;

Ett annat lösningssätt är att använda avsmalning för att få customer att betraktas som antingen en Person eller en Cat genom att kontrollera saker om det. Det enklaste sättet att göra detta är, vid första anblicken, helt i motsats till hela den första delen av detta svar om öppna typer:

const liveCount = "liveCount" in customer ? customer.liveCount : undefined;
// const liveCount: number | undefined

Va? Ja, du kan använda in-operatorn som en typvakt som snävar in customer från Customer till Cat (om "liveCount" in customer är sant) eller Person (om det är falskt). Denna typvakt implementerades i microsoft/TypeScript#15256 och den relevanta kommentaren där antyder att folk behöver ett sätt att göra detta och det är tillräckligt indikativt för avsikt att vi låter dem göra det, även om det tekniskt sett är osäkert:

Verkligheten är att de flesta unioner redan är korrekt åtskilda och inte aliaserar tillräckligt för att problemet ska uppstå. Någon som skriver ett in-test kommer inte att skriva en ”bättre” kontroll; allt som faktiskt händer i praktiken är att folk lägger till type assertions eller flyttar koden till en lika osäker användardefinierad typ-predicate. I slutändan tror jag inte att detta är värre än status quo (och det är bättre eftersom det leder till färre användardefinierade typ-predikater, som är mottagliga för fel om de skrivs med in på grund av brist på felstavningskontroll).

Det leder till nästa sätt att göra detta: skriv din egen typvakt via en funktion som returnerar ett typ-predikat.

function isCat(x: any): x is Cat {
  return x && typeof x.liveCount === "number" && typeof x.name === "string"; 😸
}

const liveCount = isCat(customer) ? customer.liveCount : undefined;
// const liveCount: number | undefined

Genom att skriva isCat() som returnerar x is Cat, talar vi om för kompilatorn att det är tillåtet att använda isCat() för att begränsa något till antingen Cat (om sant) eller något som inte är en Cat (om falskt). När du kallar isCat(customer) betyder det antingen Cat eller Person. Jag råkade implementera isCat() på det mest typesäkra sättet möjligt, men jag tog på mig det ansvaret och kompilatorn avstod från det. Man kan lika gärna skriva

function isCat(x: any): x is Cat {
  return Math.random() < 0.5 // 🙀
}

och kompilatorn skulle inte vara ett dugg klokare. Så där har du det.


SAMMANFATTNING

  • Du kan endast säkert komma åt egenskaper som finns på alla medlemmar av en union.
  • Beteendet du såg där det fungerar är en bugg. Räkna inte med det.
  • Konstiga dåliga saker kan hända om du antar att typer i TypeScript är exakta.
  • Men de kommer förmodligen inte att hända, så du kan använda typtolkningar för att få kompilatorn att sluta klaga.
  • Eller så kan du använda typavsmalning för att få kompilatorn att sluta klaga, med varierande grader av säkerhet.

Lekplatslänk till kod

Relaterade artiklar