Typescript ändrar typinfernce av returvärdet beroende på om det finns en variabel


Jag är förvirrad av koden nedan:

type TestSymbol = 'a' | 'b';
type TestType = { test: TestSymbol }

function test1(): TestType {  // no error
  return { test: 'a' };
}

function test2(): TestType {  // tsserver error 2322
  const x = { test: 'a' };
  return x;
}

Felmeddelandet är:

Type '{ test: string; }' is not assignable to type 'TestType'.
  Types of property 'test' are incompatible.
    Type 'string' is not assignable to type 'TestSymbol'.
[tsserver: 2322]

Det verkar som att TypeScript klassificerar test-egenskapen i returobjektet olika i test1 och test2. I test1 förstår det att värdet 'a' är en TestSymbol. Men i test2 klassificerar det det bara som en sträng, vilket orsakar typfelet.

Varför gör det här och hur kan jag få TypeScript att förstå att test är en TestSymbol i test2?

Svar #1

När det inte finns någon explicit typannotering på en variabel eller annat uttryck, kommer TypeScript-kompilatorn att härleda dess typ baserat på vissa heuristiska regler som är en rimlig gissning om den avsedda typen. Dessa regler är inte perfekta, men de är användbara.

I ditt första exempel,

function test1(): TestType {  
  return { test: 'a' };
}

kompilatorn härleder typen av {test: 'a'} till TestType kontextuellt genom returtypen för funktionen. Kontextuell typinferens betyder att kompilatorn härleder typen av ett uttryck baserat på vilken typ som förväntas finnas där. Detta händer endast under specifika omständigheter, och kan inte ske om en typ redan har härledts för ett värde.

Till exempel, när du deklarerar en variabel som:

const x = { test: 'a' };

kompilatorn härleder omedelbart typen för x baserat på värdet {test: 'a'}. Den väntar inte tills den ser x använd någonstans för att ta reda på vilken typ som skulle vara bäst. Den härledda typen är {test: string}. Även om x är en const, kan egenskapen x.test ändras, så kompilatorn antar att x.test kan vara vilken string som helst. Ofta är sådana antaganden korrekta, men i ditt fall är de inte.


För att åtgärda detta kan du antingen uttryckligen ange typen för x:

function testAnnotation(): TestType {
  const x: TestType = { test: 'a' }; // <-- explicit annotation
  return x; // okay
}

eller så kan du använda en const-påstående för att ändra inferensheuristiken så att kompilatorn antar att inget om x kommer att förändras:

function testConstAssertion(): TestType {
  const x = { test: 'a' } as const; // <-- ask for narrowest possible type
  // const x: { readonly test: "a"; } 
  return x; // okay
}

Den härledda typen för x är nu { readonly test: "a"; }; kompilatorn antar att test-egenskapen aldrig kommer att ändras och att dess enda möjliga värde är "a". Detta ses som tilldelningsbart till TestType (det faktum att readonly-egenskaper kan tilldelas inte-readonly-egenskaper är lite konstigt, men det är avsedd beteende och du kan läsa ett förslag om att ändra detta på microsoft/TypeScript#13347) och det finns inget kompileringsfel.

Båda sätten borde fungera.


Observera att jag inte skulle rekommendera att använda en typkonvertering av formen:

function testLessSafeTypeAssertion(): TestType {
  const x = { test: 'a' } as TestType;
  return x;
}

Även om detta kommer att kompilera, är det mindre säkert, eftersom du bara talar om för kompilatorn att {test: "a"} är en TestType istället för att be kompilatorn att verifiera det. Typantaganden tenderar att missa vissa fel:

const x = { test: Math.random() < 0.5 ? 'a' : 'z' } as TestType; // no error!

där typanteckningar fångar dem:

const x: TestType = { test: Math.random() < 0.5 ? 'a' : 'z' }; // error

Typpåståenden är främst användbara i situationer där kompileraren inte kan verifiera typen av ett värde, och du behöver ge kompileraren den information den inte har. I det här fallet kan kompileraren själv kontrollera att x är en TestType, så en annotering eller ett const-påstående är säkrare.


Lekplatslänk till kod

Svar #2

Eftersom i test1 returneras objektet direkt och typen härleds baserat på funktionens returtyp. Du kan manuellt ange typen när du använder en variabel så att du inte är beroende av typavledning:

function test2(): TestType {
  const x = { test: 'a' } as TestType;
  return x;
}

Eller annotera explicit typen av variabel:

function test2(): TestType {
  const x: TestType = { test: 'a' };
  return x;
}

lekplats

Relaterade artiklar