typescript - Varför kan jag anropa array.some() men inte array.every() med föreningstyper för arrayer?


Så, föreställ dig att jag har en (dum) funktion som denna:

function doSomething(input: number|string): boolean {
  if (input === 42 || input === '42') {
    return true;
  } else {
    return false;
  }
}

Varför får jag kalla array.some() så här:

function doSomethingWithArray(input: number[]|string[]): boolean {
  return input.some(i => doSomething(i));
}

Men inte array.every() som detta:

function doEverythingWithArray(input: number[]|string[]): boolean {
  return input.every(i => doSomething(i));
}

Vilket ger mig detta fel:

Detta uttryck kan inte anropas. Varje medlem av unionstypen '{ (predicate: (value: number, index: number, array: number[]) => value is S, thisArg?: any): this is S[]; (predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): boolean; } | { ...; }' har signaturer, men ingen av dessa signaturer är kompatibla med varandra.

Jag förstår inte skillnaden. I mitt huvud borde antingen båda fungera, eller ingen av dem. Vad missar jag?

Observera att doSomething() accepterar number|string som argument, så det borde fungera med varje element av number[]|array[], precis som det gör för array.some(), eller hur?

Svar #1

Detta är i huvudsak en nuvarande begränsning i TypeScript; se microsoft/TypeScript#44373.


TypeScript har länge haft ett problem med att anropa metoder på en union av array-typer. Ursprungligen kunde man inte anropa unions-av-funktioner alls, eftersom kompilatorn inte visste hur man skulle slå samman flera signaturer till en enda användbar. Se microsoft/TypeScript#7294:

// Before TS3.3:
declare const arr: string[] | number[];
arr.some(() => true); // error
arr.map(() => 1); // error
arr.every(() => true); // error

TypeScript 3.3 introducerade visst stöd för att göra detta med microsoft/TypeScript#29011, genom att acceptera en snittmängd av parametrarna från unionens medlemmar.

Men detta stöd lades bara till för relativt enkla fall där högst en medlem i unionen var en generisk funktion och högst en medlem i unionen var en överbelastad funktion. Så om arraymetoderna du försökte slå ihop var generiska eller överbelastade kunde du fortfarande inte anropa dem. Så det var en förbättring, men vissa metoder som map() gick fortfarande inte att anropa. Se microsoft/TypeScript#36390:

// TS3.3
declare const arr: string[] | number[];
arr.some(() => true); // okay
arr.map(() => 1); // error
arr.every(() => true); // okay

Vid någon punkt beslutades det att lägga till en generisk överbelastning till every() så att den kunde fungera som en typvaktfunktion; se microsoft/TypeScript#38200. Detta är användbart, men tyvärr innebär det att every() också slutade fungera på unioner av array-typer. Detta släpptes med TypeScript 4.0:

// TS 4.0
declare const arr: string[] | number[];
arr.some(() => true); // okay
arr.map(() => 1); // error
arr.every(() => true); // error

För TypeScript 4.2 slogs microsoft/TypeScript#31023 samman, vilket lade till viss support för att anropa unioner av generiska anropssignaturer, så länge de generiska typparametrarna var identiska. Men för överbelastade anropssignaturer kvarstår problemet:

// TS 4.2+
declare const arr: string[] | number[];
arr.some(() => true); // okay
arr.map(() => 1); // okay
arr.every(() => true); // error 

Och det är där vi befinner oss just nu.


Kanske kommer microsoft/TypeScript#44373 att tas upp vid någon tidpunkt. Det är inte självklart vad som är rätt att göra här för unioner av överlagrade metoder i allmänhet. Jag tror att det är möjligt att någon kan vilja rikta sig endast mot fallet med unioner-av-arrayer genom att automatiskt utvidga till en readonly-array-av-unions, eftersom att göra det manuellt är den lösning jag vanligtvis föreslår, och det är ganska säkert:

const arr2: readonly (string | number)[] = arr; // okay
arr2.every(() => true); // okay

Men för tillfället händer detta inte.

Playgorund-länk till kod

Svar #2

Som vi redan diskuterade i kommentarerna, har every en överlagring som innehåller en generisk funktion med ett typvillkor medan some inte har något sådant.

Varför är funktionssignaturerna "inkompatibla" nu?

  • Det är definitivt relaterat till det faktum att de är generiska. Jag har ännu inte hittat ett definitivt svar. Kanske @jcalz vet en tillfredsställande förklaring :)

Varför är every generisk och har ett typ-predikat?

  • Till skillnad från some, kan anropet every påverka typen av arrayen. Om du har en array som

    const arr: (string | number)[] = []
    

    Att kalla every på den och kontrollera om varje element är ett nummer bör också ändra typen på den resulterande arrayen.

    const arr2 = arr.every((e): e is number => typeof e === "number")
    // arr2 should be number[]
    

    Detta är endast möjligt att uppnå med typ-predikat. Och för att tillåta typ-predikat att ha en effekt här måste every vara generisk. (Annars skulle typ-predikatet ignoreras av every)


Så vad kan du göra åt det?

Jag skulle föreslå att ändra typen på input till en skärning av (number[] | string[]) och (number | string)[]. Detta kan se konstigt ut men det rättar till felet här.

function doEverythingWithArray(
  input: (number[] | string[]) & (number | string)[]
): boolean {
  return input.every(i => doSomething(i));
}

Funktionen förblir också anropbar som tidigare.

// works as before
doEverythingWithArray(["a", "b", "c"])
doEverythingWithArray(["a", "b", "c"] as string[])
doEverythingWithArray([0, 1, 2])
doEverythingWithArray([0, 1, 2] as number[])

// fails
doEverythingWithArray([0, 1, "a"]) 

Lekplats

Relaterade artiklar