typescript - Hur kan jag implementera en funktion med flera anropssignaturer?


Jag vill begränsa min funktion.

Givet en viss literal typ som input, bör endast en viss mall-literaltyp returneras som output för två av inputfallen.

Till exempel, givet indatan "rain" vill jag ha utdata "umbrella_foo".

För att åtgärda detta skapade jag denna överbelastade funktion:

type Options = "rain" | "sun" | "wind" | "snow";

type OverloadedFunction = {
  (option: 'rain'): `umbrella_${string}`;
  (option: 'sun'): `shorts_${string}`;
  (option: Options): string;
};

const myFunctionFoo: OverloadedFunction = (options: Options) => {
  if (options === "rain") {
    return 'umbrella_foo';
  }
  if (options === "sun") {
    return 'shorts_foo';
  }
  return 'anything'
}

OverloadedFunction verkar okej men tyvärr när jag håller muspekaren över myFunctionFoo ser jag TypeScript-felet:

Type '(options: Options) => "umbrella_foo" | "shorts_foo" | "anything"' is not assignable to type 'OverloadedFunction'.
  Type '"umbrella_foo" | "shorts_foo" | "anything"' is not assignable to type '`umbrella_${string}`'.
    Type '"shorts_foo"' is not assignable to type '`umbrella_${string}`'.(2322)

Varför vill TypeScript tilldela "shorts_foo" till något av typen umbrella_$ {string}?

På något sätt ignorerar TypeScript if/else-villkoret. Det är inget annorlunda med en switch/case-sats.

TypeScript-lekplats för ovanstående kod.

Svar #1

Det här har inte att göra med strängliteraltyper eller mallliteraltyper. Snarare handlar det om hur kompilatorn kontrollerar tilldelbarhet för överlagrade funktionstyper som har flera anropssignaturer. Se microsoft/TypeScript#33482 för ett kanoniskt svar.


För att klargöra frågan, låt mig skriva om ditt exempel utan några litteraler eller mall-litteraler:

type OverloadedFunction = {
  (x: string): string;
  (x: number): number;
};

const arrowFn: OverloadedFunction // error!
  //  ~~~~~~~ <-- Type '(x: string | number) => string | number' 
  // is not assignable to type 'OverloadedFunction'.
  = (x: string | number) => x;

Här har vi samma problem. Kompilatorn kan inte se att initialiseringen för arrowFn överensstämmer med OverloadedFunction. Värdet (x: string | number) => x antas vara av typen (x: string | number) => string | number, vilket är för brett för att kunna tilldelas OverloadedFunction.

Faktiskt, om allt du visste om en funktion var att den accepterar string | number och returnerar string | number, skulle du inte vara säker på att använda den där en OverloadedFunction behövs. Till exempel:

const sameProblem: OverloadedFunction // error!
  //  ~~~~~~~~~~~ <-- Type '(x: string | number) => string | number' 
  // is not assignable to type 'OverloadedFunction'.
  = (x: string | number) => Math.random() ? String() : Number();

I det här fallet är felet ett bra ett, eller hur? Funktionen returnerar bara slumpmässigt en string eller en number utan att alls ta hänsyn till inmatningsvärdet, vilket inte är vad en OverloadedFunction gör.

Enligt en kommentar på en dubblett av microsoft/TypeScript#33482,

TypeScript kan inte analysera funktionskroppen för att avgöra vilka kodvägar som tas under vissa överlagrade fall, så det kan bara titta på den övergripande härledda returtypen, vilket faktiskt är bredare än den specificerade returtypen. Det är inte möjligt att skriva detta tilldelningsmönster utan en typassertion på minst ett ställe.

Sådan analys är inte omöjlig att genomföra, men det skulle försämra kompilatorns prestanda; tiden för att typkontrollera funktioner skulle uppenbarligen skala med kvadraten av antalet anropssignaturer (jfr denna kommentar om ett relaterat problem på GitHub), och därför är kostnaden för att göra det större än fördelen.

Om du vill tilldela ett funktionsuttryck till en överlagrad funktionstyp och stöter på detta problem, måste du använda en typanspråkning för att övertyga kompilatorn om att allt är okej:

const arrowFnAssert
  = ((x: string | number) => x) as OverloadedFunction;

Men notera att pilfunktioner generellt inte är sättet som människor implementerar överlagringar på. Som du har sett tenderar kompilatorn att ge falska positiva eftersom den är för försiktig för att tillåta det som visar sig vara en säker implementering.

Istället implementerar människor vanligtvis överlagringar via funktionssatser, vilka kontrolleras mer löst:

// call signatures
function statementFn(x: string): string;
function statementFn(x: number): number;

// implementation
function statementFn(x: string | number) {
  return x; // no error
}

const reassigned: OverloadedFunction =
  statementFn; // okay

Nu finns det inget fel. Detta betyder dock inte att det är noggrant kontrollerat. Det skulle fortfarande vara för dyrt för kompilatorn. Det är bara det att för överlagrade funktionssatser tenderar kompilatorn att ta till den säkra sidan och ge falskt negativa, och därför misslyckas den med att fånga fel som detta:

function oops(x: string): string;
function oops(x: number): number;
function oops(x: string | number) {
  return Math.random() ? String() : Number(); // no error

För det problemet, se microsoft/TypeScript#13235, där det återigen förklaras att den "riktiga" kontrollen är orimligt kostsam.


Hur som helst, kompilatorn kan egentligen inte avgöra vad som är och inte är en säker implementering för en överlagrad funktion. Du måste använda en typassertion eller ett uttalande för en överlagrad funktion om du vill fortsätta utan ett kompileringsfel, och i båda fallen måste du vara försiktig med att göra rätt kontroll själv eftersom kompilatorn inte kan det.

Lekplatslänk till kod