TypeScript - Tilldelningsregler för unionstyper av funktioner


Låt oss ta exemplet från TS 3.3-dokumentationen:

type Fruit = "apple" | "orange";
type Color = "red" | "orange";

type FruitEater = (fruit: Fruit) => number;
type ColorConsumer = (color: Color) => string;

type FnUnion = FruitEater | ColorConsumer

Jag trodde att följande funktion skulle kunna tilldelas FnUnion :

declare function foo(v: Fruit & Color): number | string

, som någon berättade för mig, är funktioner kontravarianta i parametrarna och kovarianta i returtypen. Men överväg följande:

declare let fn: FnUnion
declare function foo(v: Fruit & Color): number | string
declare function foo2(v: Fruit | Color): number
declare function foo3(v: Fruit | Color): string

fn = foo // error, why?
fn = foo2 // works
fn = foo3 // works

Jag är intresserad av de allmänna tilldelningsreglerna för unionfunktioner i ovanstående fall. foo2 och foo3 kan tilldelas fn. Men varför kan inte foo tilldelas?

Jag är säker på att jag kommer att missa viss TS-kunskap här, uppskattar svar. Tack! PS: Här är ett exempel.

Svar #1

Bara för att vara tydlig, & är snitt och | är union. Detta betyder att:

Fruit & Color //=> "orange"
Fruit | Color //=> "apple" | "orange" | "red"

Vilket betyder att detta:

type FnUnion = FruitEater | ColorConsumer

Är en funktion som accepterar alla frukter, eller en funktion som accepterar alla färger.

Följande är giltig användning av den typen:

const colorFn: FnUnion = (color: Color) => 'a color'
colorFn('red')
colorFn('orange')

const fruitFn: FnUnion = (fruit: Fruit) => 123
fruitFn('apple')
fruitFn('orange')

Obs: det måste vara det ena eller det andra. Det måste acceptera alla frukter eller det måste acceptera alla färger.

Dock kan typen Fruit & Color endast vara 'orange'. Så du tilldelar en funktion som endast kan acceptera 'orange' till en typ som tillåter acceptering av antingen 'orange'|'red' eller 'orange'|'apple'. Detta innebär att 'red' eller 'apple' aldrig skulle kunna skickas som ett argument, där FnUnion säger att det ena eller det andra borde tillåtas.

Och så klagar TypeScript, med rätta, på att typerna inte är kompatibla.

Dessutom känner Typescript till unionstypen, men också vilket medlem av unionen det vet att värdet är vid vissa punkter i kontrollflödet.

När du kastar till FnUnion på det här sättet:

const colorFn = ((color: Color) => 'a color') as FnUnion

Du säger till TS att vilken medlem av unionen det är är okänt, eftersom den inte längre kan diskriminera baserat på argumenttyp.

Därför verkar det som om den enda säkra slutsatsen är det enda argument som de två typerna har gemensamt. Vilket, inser jag, är precis den funktionstyp som det inte skulle låta dig tilldela direkt.

Men jag tror att den avgörande skillnaden är att veta vilken medlem av unionen din funktion är.

// member known: ColorConsumer
const colorFn: FnUnion = (color: Color) => 'a color'

// member known: FruitEater
const fruitFn: FnUnion = (fruit: Fruit) => 123

// member unknown due to cast to less specific type.
//   only common arguments are allowed,
//   and only return types for those common arguments
const unknownFn: FnUnion = ((some: 'orange') => 'unknown') as FnUnion

// type is concretely known, but is not a match to any member.
const invalidFn: FnUnion = (some: 'orange') => 'invalid'
// ^ type error

FnUnion säger att det är det ena eller det andra, och om det inte vet vilket av dem, då är det snittet av båda. Det du försöker säga är att typen är känd och det är inte någon av medlemmarna, utan snittet av båda. Det här är vad som är inkompatibelt.

Men som du upptäckte med as FnUnion kan du få TypeScript att glömma vad det vet om något med en typkonvertering.

Allt detta betyder att vilken typ något härleds till att vara inte alltid är den typ som det skulle kunna tilldelas att vara.

Svar #2

Om ett värde kan tilldelas en unionstyp S | T måste det antingen kunna tilldelas S, eller kunna tilldelas T, eller båda. Nu överväg följande funktion:

function foo(v: Fruit & Color): number | string {
    return Math.random() > 0.5 ? 23 : 'bar';
}

Funktionen foo kan inte tilldelas typen FruitEater, eftersom den ibland returnerar en sträng; och den kan heller inte tilldelas typen ColorConsumer, eftersom den ibland returnerar ett nummer. Därför, eftersom foo inte kan tilldelas någon av typerna, kan den inte heller tilldelas unionen av dessa två typer. QED.

Men det finns ett annat problem: även om vi ändrar returtyperna för FruitEater, ColorConsumer och foo så att de alla är samma, så går foo fortfarande inte att tilldela unionen. Den äter inte 'apple' så den är inte en FruitEater, och den konsumerar inte 'red' så den är inte en ColorConsumer.

Så i allmänhet är unionen av två funktionstyper (arg: A) => B och (arg: C) => D en subtyp av funktionstypen (arg: A & C) => (B | D), men inte identisk med den.