Anonym överbelastad funktionstyp i TypeScript fungerar inte, men det fungerar med en namngiven funktion


Jag har följande TypeScript-kod som inte kompilerar:

type MyFunctionBaseOptions = {
  some_optional_option?: string
}

type MyFunction = {
  (options: MyFunctionBaseOptions) : number[]
  (options: MyFunctionBaseOptions & ({callback: () => void})) : null
}

const myFunction : MyFunction = (
  options: MyFunctionBaseOptions | (MyFunctionBaseOptions & ({callback: () => void}))
) : number[]|null => {
  if ("callback" in options) {
    return null
  }

  return [1, 2]
}

Ger felet:

Typen '(options: MyFunctionBaseOptions | (MyFunctionBaseOptions & ({ callback:() => void; }))) => number[] | null' kan inte tilldelas typen 'MyFunction'. Typen 'number[] | null' kan inte tilldelas typen 'number[]'. Typen 'null' kan inte tilldelas typen 'number[]'.

Att använda en namngiven funktion med samma överlagringar fungerar:

type MyFunctionBaseOptions = {
  some_optional_option?: string
}

function myFunction(options: MyFunctionBaseOptions) : number[]
function myFunction(options: MyFunctionBaseOptions & ({callback: () => void})) : null

function myFunction(
    options: MyFunctionBaseOptions | (MyFunctionBaseOptions & ({callback: () => void}))
) : number[]|null {
  if ("callback" in options) {
    return null
  }

  return [1,2]
}

Men jag behöver ha den överlagrade funktionen som en typ, inte en namngiven funktion så att jag kan göra något som:

function myFunctionFactory() : MyFunction {
    return function(/* ... */) { /* ... */ }
}

Att använda den namngivna funktionen och "extrahera" typen med typeof ger också samma fel som i det första kodexemplet:

type MyFunctionBaseOptions = {
  some_optional_option?: string
}

function myFunction(options: MyFunctionBaseOptions) : number[]
function myFunction(options: MyFunctionBaseOptions & ({callback: () => void})) : null

function myFunction(
    options: MyFunctionBaseOptions | (MyFunctionBaseOptions & ({callback: () => void}))
) : number[]|null {
  if ("callback" in options) {
    return null
  }

  return [1,2]
}

//                       vv--- doesn't work, same error as first code example
const mySecondFunction : typeof myFunction = (
  options: MyFunctionBaseOptions | (MyFunctionBaseOptions & ({callback: () => void}))
) : number[]|null => {
  if ("callback" in options) {
    return null
  }

  return [1, 2]
}

Är detta en begränsning i TypeScript eller finns det ett sätt att komma runt detta?

(Ja, jag skulle kunna använda const myFunction : MyFunction=(...) as MyFunction men då skulle typkontrollen minskas, vilket inte är vad jag vill/söker).

Svar #1

Ja, TypeScript kontrollerar överlagrade funktioner olika beroende på om de är function-satser eller function-uttryck (inklusive pilfunktioner). Funktioner kontrolleras något slafsigt, så länge returvärdena matchar någon anropssignatur så godkänns det (även om du returnerar något för fel signatur).

function foo(x: { a: string }): { b: number };
function foo(x: { b: number }): { a: string };
function foo(x: { a: string } | { b: number }) {
  return "a" in x ? { b: x.a.length } : { a: x.b.toFixed() }
}

function bar(x: { a: string }): { b: number };
function bar(x: { b: number }): { a: string };
function bar(x: { a: string } | { b: number }) {
  return x; // this compiles !!!!
}

Å andra sidan kontrolleras funktionsuttryck för strikt, där om inte returvärdena matchar alla anropssignaturer, kommer det att misslyckas:

type FooType = {
  (x: { a: string; }): { b: number; };
  (x: { b: number; }): { a: string; };
}

const baz: FooType = x => "a" in x ? { b: x.a.length } : { a: x.b.toFixed() }; // error!
//    ~~~
const qux: FooType = x => x // error!
//    ~~~
const quux: FooType = x => ({ a: "", b: 1 }); // okay

Så för överbelastningar måste du i princip välja om du vill att de ska kontrolleras för lite eller för mycket. Det finns inget alternativ att kontrollera dem helt korrekt.


Det verkar som att du har några funktionsuttryck och vill se dem kontrolleras löst, som funktionssatser. Det finns en funktionförfrågan på microsoft/TypeScript#47669 för att stödja detta, men tills det implementeras måste du arbeta runt det.

Du kan kringgå det antingen genom att använda typförsäkringar:

const baz =
  (x => "a" in x ? { b: x.a.length } : { a: x.b.toFixed() }) as FooType; // okay

eller genom att refaktorera för att använda funktionssatser inuti något inre scope:

const baz: FooType = (() => {
  function foo(x: { a: string }): { b: number };
  function foo(x: { b: number }): { a: string };
  function foo(x: { a: string } | { b: number }) {
    return "a" in x ? { b: x.a.length } : { a: x.b.toFixed() }
  }
  return foo;
})()

Du säger att du inte vill använda typförsäkringar eftersom de ger upp typstryggheten, men det är inte klart att du förlorar så mycket, eftersom överbelastade funktionsdeklarationer redan är något osäkra, som jag har visat ovan.

Ändå, för din fabriksfunktion, är det en enkel förändring att använda funktionsuttalanden inuti fabriken; det finns ingen anledning till varför du inte kan begränsa ett funktionsuttalande inuti en annan funktion:

function myFunctionFactory() {
  function myFunction(options: MyFunctionBaseOptions): number[]
  function myFunction(options: MyFunctionBaseOptions & ({ callback: () => void })): null
  function myFunction(
    options: MyFunctionBaseOptions | (MyFunctionBaseOptions & ({ callback: () => void }))
  ): number[] | null {
    if ("callback" in options) {
      return null
    }
    return [1, 2]
  }
  return myFunction;
}
type MyFunction = ReturnType<typeof myFunctionFactory>

Playground-länk till kod

Svar #2

Eftersom du vill att din MyFunction-typannotering ska bete sig som en fabrik, föreslår jag att använda en funktionsfabrik istället, och ja, jag har inget emot att ha några mycket begränsade TS-fel som jag kan kontrollera.

I det här fallet är din funktion korrekt typad och insnävning fungerar också. Det enda problemet är att returtypen inte kontrolleras och du bör ta hand om den själv (null | number[]).

Lekplats

type MyFunctionBaseOptions = {
  some_optional_option?: string
}
type MyFunctionBaseOptionsCb = MyFunctionBaseOptions & {
  callback: () => void
}

function makeFunction(cb: (options: MyFunctionBaseOptions | MyFunctionBaseOptionsCb) => null | number[]){

  function out(fn: (options: MyFunctionBaseOptions) => number[]): typeof fn;
  function out(fn: (options: MyFunctionBaseOptionsCb) => null): typeof fn;
  function out(fn: any){ return fn; }
  // @ts-expect-error
  return out(cb);

}

const myFunction = makeFunction((
  options
) => {
  if ('callback' in options) {
    return null;
  }
  return [1, 2]
})

const c = myFunction({callback:() =>{}});