Home > Blockchain >  Narrow function type from union of call signatures by return type
Narrow function type from union of call signatures by return type

Time:10-30

I'm trying to make a type that is basically call signature overloading, but based on the return type instead of the arguments.

type Callback<T> = (value: T) => void

type CallbackHandler<TResult> = (event: object, cb: Callback<TResult>) => void
type AsyncHandler<TResult> = (event: object) => Promise<TResult>

type Handler<TResult> = CallbackHandler<TResult> | AsyncHandler<TResult>

const cbHandler: Handler<number> = (e1, cb1) => {
  // this works
  cb1(1)
}
cbHandler({}, () => {})

const asyncHandler: Handler<number> = async (e2) => {
  // here e2 is implicitly typed as `any`
  // it shouldn't because the function returns Promise<number>, not void
  return 1
}
asyncHandler({}) // this should be callable with 1 argument

Playground Link

Is this at all possible? Or maybe this should be filed as a TS bug / feature request?

CodePudding user response:

The problem here is that functions with fewer parameters are assignable to functions with more parameters, and functions returning non-void are assignable to functions returning void. These are both intentional behaviors that are useful and type safe, but they are sometimes surprising and have frustrating consequences.

This implies that the following assignments are valid:

const f = (event: object) => Promise.resolve(3);
const c: CallbackHandler<number> = f; // okay
const a: AsyncHandler<number> = f; // okay

And thus when the compiler looks at this:

const asyncHandler: Handler<number> = async (e2) => { return 1 }
// we'll ignore this error until later ----> ~~

It sees that asyncHandler is assignable to both CallbackHandler<number> and AsyncHandler<number> and does not narrow it. That means the following error is as expected:

asyncHandler({}) // error

If you want to avoid that you should probably just annotate asyncHandler as AsyncHandler<number>:

const asyncHandler: AsyncHandler<number> = async (e2) => { return 1 }
asyncHandler({}) // okay

Of course that doesn't clear up why there's an implicit any on the e2 parameter.

If you implement a union of function types you need to accept a union of their arguments (note that this is the dual situation to when you try to call a union of function types and you need to pass in an intersection of their arguments, support for which was implemented in TypeScript 3.3). But instead of the compiler contextually typing the callback parameter as being of such a union (which would be object | object or just object in your case), the compiler just gives up on contextual typing and falls back to any.

And even if you change the definitions so that narrowing does happen (instead of a void return, put something incompatible), you still have the same error:

const stillAProblem: ((x: string) => number) | ((x: string, y: number) => string) =
  x => 3; // error! implicit any

stillAProblem("").toFixed(); // okay

This seems to be a design limitation or bug of TypeScript.

microsoft/TypeScript#6357 back in 2016 just says "unions of functions don't give you contextual typing" as if it is a fact of life, which it probably was back then. Since then, microsoft/TypeScript#37580 ran into this and calls it a bug. And microsoft/TypeScript#41213 also ran into this and calls it a bug. So maybe it's a known bug. But I don't know when or if it will be addressed.

The expedient thing for you to do, if this were your only problem, would be to annotate the e2 callback parameter explicitly:

const asyncHandler: Handler<number> = async (e2: object) => { return 1 }

But of course that doesn't fix the original issue from above:

asyncHandler({}) // still an error

so my suggestion here is still to annotate asyncHandler itself as AsyncHandler<number> and be done with it.

Playground link to code

CodePudding user response:

I think this is because asyncHandler is explicitly marked as Handler<number>. It's definition won't narrow it to AsyncHandler.

Take a look at following simple example:

const foo: object = {bar: 'abc'};

foo.bar; //Property 'bar' does not exist on type 'object'.

Here for TS foo is just an object.

  • Related