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
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.
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.