return 类型的调用签名联合缩小函数类型

Narrow function type from union of call signatures by return type

我正在尝试创建一个基本上是调用签名重载的类型,但基于 return 类型而不是参数。

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

这可能吗?或者也许这应该作为 TS 错误/功能请求提交?

我认为这是因为 asyncHandler 被明确标记为 Handler<number>。它的定义不会将其缩小到 AsyncHandler.

看看下面这个简单的例子:

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

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

这里对于 TS foo 来说只是一个对象。

这里的问题是functions with fewer parameters are assignable to functions with more parameters, and functions returning non-void are assignable to functions returning void。这些都是有用且类型安全的有意行为,但它们 有时会令人惊讶并产生令人沮丧的后果。

这意味着以下分配有效:

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

因此当编译器查看时:

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

它看到 asyncHandler 可以分配给 CallbackHandler<number>AsyncHandler<number> 并且不会缩小它。这意味着以下错误符合预期:

asyncHandler({}) // error

如果你想避免这种情况,你应该将 asyncHandler 注释为 AsyncHandler<number>:

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

当然,这并不能解释为什么 e2 参数上有一个隐含的 any

如果您实现函数类型的联合,您需要接受它们参数的联合(请注意,这是您尝试 调用 函数联合时的双重情况类型,您需要传入它们参数的 交集 support for which was implemented in TypeScript 3.3). But instead of the compiler contextually typing 回调参数属于此类联合(即 object | object 或只是 object 在你的情况下),编译器只是放弃上下文类型并回退到 any.

并且即使您更改定义以便确实发生缩小(而不是 void return,放一些不兼容的东西),您仍然会遇到相同的错误:

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

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

这似乎是 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 也 运行 进入这个并称它为错误。所以也许这是一个已知的错误。但我不知道什么时候或是否会得到解决。

如果这是您唯一的问题,您可以做的权宜之计是显式注释 e2 回调参数:

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

但这当然不能解决上面的原始问题:

asyncHandler({}) // still an error

所以我这里的建议还是把asyncHandler本身注解成AsyncHandler<number>就完事了

Playground link to code