TypeScript-intersektionstyper av funktion


Jag använder TypeScript och stötte på några problem. Den enklaste demon skulle vara:

type g = 1 & 2 // never
type h = ((x: 1) => 0) & ((x: 2) => 0) // why h not never
type i = ((x: 1 & 2) => 0)// why x not never

Jag förstår inte varför typen h inte är never och parametern x i typen i inte är never

type e = (((x: 1) => 0) & ((x: 2) => 0)) extends (x: infer L) => 0 ? L : never; //  why e is 2 not never or 1?

Förstod också inte varför typ e är 2 och inte never?

Svar #1

Observera: icke-primitiva typer skrivs konventionellt med UpperCamelCase; detta skiljer dem från primitiva typer och variabel-/egenskapsnamn. Jag kommer att använda denna konvention i det som följer.

Det finns en massa delar i den här frågan, och det kommer därför att finnas en massa delar i dess svar.


Låt oss ta de enkla först:

type G = 1 & 2 // never

Reduktionen till never av tomma snitt som 1 & 2 implementerades i microsoft/TypeScript#31838 och släpptes med TypeScript 3.6. Före detta behandlades 1 & 2 mycket likt never, eftersom du aldrig skulle kunna hitta ett värde som uppfyller det. Det finns egentligen ingen konceptuell skillnad mellan 1 & 2 och never, även om detaljer i kompilatorns implementation kan göra att den ena behandlas annorlunda än den andra.


Nästa:

type I = ((x: 1 & 2) => 0) // why x not never

x är aldrig, men reduktionen är uppskjuten tills du faktiskt använder den:

type IParam = Parameters<I>[0]; // never

Denna uppskov implementerades i microsoft/TypeScript#36696 och släpptes med TypeScript 3.9. Innan detta reducerades x ivrigt till never precis som G ovan.


Nu för några mer komplicerade exempel:

type H = ((x: 1) => 0) & ((x: 2) => 0) // why H not never

Det finns faktiskt ganska många anledningar till varför H inte är never:

  • Den TypeScript-specifika anledningen är att en intersection av funktionstyper betraktas som samma sak som en överlagrad funktion med flera anropssignaturer:

    declare const h: H;
    // 1/2 h(x: 1): 0
    // 2/2 h(x: 2): 0
    h(1); // okay
    h(2); // okay
    h(3); // error, no overload matches this call
    h(Math.random() < 0.5 ? 1 : 2); // error, no overload matches this call
    

    Observera hur h visas som en funktion med två överlagringar; en som accepterar en 1-parameter och en annan som accepterar en 2-parameter. Den accepterar inte 3, och den accepterar inte heller 1 | 2 även om den ur ett rent typ-systemperspektiv förmodligen borde göra det (se microsoft/TypeScript#14107 för en långvarig funktionsförfrågan om att stödja detta).

  • Även om TypeScript inte tolkade funktionssnitt som överlagringar, borde H inte vara never. Funktions typer är kontravarianta i sina parametertyper (se mitt svar på den här frågan för en förklaring. Du kan också läsa om kontravarians av funktionsparametrar i TS-handbokens beskrivning av kompileringsflaggan --strictFunctionTypes). Kontravarians vänder saker till deras dual; Om F<T> är kontravariant i T, då är F<T | U> ekvivalent med F<T> & F<U>, och F<T & U> är ekvivalent med F<T> | F<U>. Så ett konsekvent typ-system, när det efterfrågas parametertypen av ett snitt av funktioner, skulle returnera unionen av funktionsparametrarnas typer. Och 1 | 2 är inte never.

  • Även om H vore en helt obebodd typ, reducerar TypeScript för närvarande endast snitt till never i vissa situationer, och ett snitt av funktioner är inte en av dessa. Om du tittar i kompilatorns källkodsfil checker.ts står det "en snittyp betraktas som tom om den innehåller typen never, eller mer än en enhetstyp eller en objekttyp och en nullable-typ (null eller undefined), eller en string-lik typ och en typ som är känd för att vara icke-string-lik, eller en number-lik typ och en typ som är känd för att vara icke-number-lik, eller en symbol-lik typ och en typ som är känd för att vara icke-symbol-lik, eller en void-lik typ och en typ som är känd för att vara icke-void-lik, eller en icke-primitive typ och en typ som är känd för att vara primitiv. Ingen av det där säger "två inkompatibla funktionstyper", så det reduceras inte till never.


Sista:

type E = (((x: 1) => 0) & ((x: 2) => 0)) extends (x: infer L) => 0 ? L : never;
//  why E is 2 not never or 1?

Kom ihåg från tidigare att ett snitt av funktioner betraktas som en överlagrad funktion. Det är en känd designbegränsning i TypeScript att när man använder typinferens på en överlagrad funktionstyp, försöker inte kompilatorn att lösa överlagringen genom att ta reda på vilken anropssignatur som bäst matchar den avsedda inferensen. Den använder bara den sista anropssignaturen och ignorerar alla de andra. I E betyder det att kompilatorn endast ser (x: 2) => 0 när den matchar mot (x: infer L) => 0, och därför infereras L som 2. Det "rätta" att göra skulle förmodligen vara unionen 1 | 2, men detta sker inte. Se microsoft/TypeScript#27027 för ett exempel på denna begränsning.


Playground-länk till kod

Relaterade artiklar