admin管理员组

文章数量:1435760

I have the following typescript code that doesn't compile:

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]
}

Gives the error:

Type '(options: MyFunctionBaseOptions | (MyFunctionBaseOptions & ({ callback: () => void; }))) => number[] | null' is not assignable to type 'MyFunction'. Type 'number[] | null' is not assignable to type 'number[]'. Type 'null' is not assignable to type 'number[]'.

Using a named function with the same overloads works:

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]
}

But I need to have the overloaded function as a type, not a named function so I can do something like:

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

Using the named function and "extracting" the type with typeof also yields the same error as the first code example:

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]
}

Is this a limitation of TypeScript or is there a way around this?

(Yes, I could use const myFunction : MyFunction = (...) as MyFunction but then the type checking would be reduced, which is not what I want / looking for).

I have the following typescript code that doesn't compile:

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]
}

Gives the error:

Type '(options: MyFunctionBaseOptions | (MyFunctionBaseOptions & ({ callback: () => void; }))) => number[] | null' is not assignable to type 'MyFunction'. Type 'number[] | null' is not assignable to type 'number[]'. Type 'null' is not assignable to type 'number[]'.

Using a named function with the same overloads works:

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]
}

But I need to have the overloaded function as a type, not a named function so I can do something like:

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

Using the named function and "extracting" the type with typeof also yields the same error as the first code example:

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]
}

Is this a limitation of TypeScript or is there a way around this?

(Yes, I could use const myFunction : MyFunction = (...) as MyFunction but then the type checking would be reduced, which is not what I want / looking for).

Share Improve this question asked Nov 17, 2024 at 0:02 MarcoMarco 7,2773 gold badges22 silver badges55 bronze badges 2
  • Yes, overloaded function expressions are not checked the way overloaded function declarations are. There's an open feature request at ms/TS#47669 to change this. Until/unless it's implemented you need to work around it. Not sure why you can't just use function declarations inside your factory, maybe as this playground link shows. Does that fully address the question? If so I'll write an answer or find a duplicate. If not, what's missing? – jcalz Commented Nov 17, 2024 at 0:11
  • @jcalz Yes! That fully answers my question! Thank you so much for the quick reply and reference to the github issue. If you post it as answer, I will happily accept it. – Marco Commented Nov 17, 2024 at 0:47
Add a comment  | 

2 Answers 2

Reset to default 1

Yes, TypeScript checks overloaded functions differently depending on whether they are function statements or function expressions (including arrow function expressions). Function statements are checked somewhat loosely, where as long as the return values match some call signature it passes (even if you return something for the wrong signature):

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 !!!!
}

On the other hand, function expressions are checked too strictly, where unless the return values match all the call signatures, it will fail:

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

So for overloads you basically have to choose whether you want to see them checked too little or too much. There's no option to check them fully accurately.


It seems you have some function expressions and want to see them checked loosely, like function statements. There's a feature request at microsoft/TypeScript#47669 to support this, but until and unless that's implemented you'll need to work around it.

You can work around it either by using type assertions:

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

or by refactoring to use function statements inside some inner 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;
})()

You say you don't want to use type assertions because they give up type safety, but it's not clear that you're losing too much, because overloaded function statements are already somewhat unsafe, as I've shown above.

Still, for your factory function, it's an easy change to use function statements inside the factory; there's no reason why you can't scope a function statement inside another function:

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 link to code

Since you want your MyFunction type annotation behave like a factory, I suggest to use a function factory rather, and yes, I don't mind to have some very narrow scope TS errors that I can control.

In this case your function is typed correctly and narrowing works also. The only problem that the return type isn't controlled and you should take care of it yourself (null | number[]).

Playground

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:() =>{}});

本文标签: