【问题标题】:How can the input type select the output and support a union?输入类型如何选择输出并支持联合?
【发布时间】:2019-03-05 14:26:29
【问题描述】:

我有以下代码,它可以采用可迭代或异步可迭代并返回相同类型的对象。它还有一个可以选择柯里化的数字。

function _buffer<T>(size: number, iterable: AsyncIterable<T>): AsyncIterableIterator<T> {
  throw new Error('not important')

}

function* syncBuffer<T>(size: number, iterable: Iterable<T>): IterableIterator<T> {
  throw new Error('not important')
}

export function buffer<T>(
  size: number
): {
  (curriedIterable: AsyncIterable<T>): AsyncIterableIterator<T>
  (curriedIterable: Iterable<T>): IterableIterator<T>
  (curriedIterable: Iterable<T> | AsyncIterable<T>): Iterable<T> | AsyncIterable<T>
}
export function buffer<T>(size: number, iterable: AsyncIterable<T>): AsyncIterableIterator<T>
export function buffer<T>(size: number, iterable: Iterable<T>): IterableIterator<T>
export function buffer<T>(size: number, iterable: Iterable<T> | AsyncIterable<T>): Iterable<T> | AsyncIterable<T>
export function buffer<T>(size: number, iterable?: Iterable<T> | AsyncIterable<T>) {
  if (iterable === undefined) {
    return <R>(curriedIterable) => buffer<R>(size, curriedIterable)
  }
  if (iterable[Symbol.asyncIterator]) {
    return _buffer(size, iterable as AsyncIterable<T>)
  }

  return syncBuffer(size, iterable as Iterable<T>)
}


function run(a: AsyncIterable<any>) {
  return buffer(4, a)
}


function run(a: AsyncIterable<any> | Iterable<any>) {
  return buffer(4, a)
  return buffer(4)(a)
}

但是我在编译时收到以下类型错误。

Overload signature is not compatible with function implementation.ts(2394) 

// in reference to 
export function buffer<T>(size: number, iterable: Iterable<T> | AsyncIterable<T>): Iterable<T> | AsyncIterable<T>

但情况似乎并非如此?如果我删除该重载签名,当我不知道它是哪个时,我无法使用联合调用该函数。

【问题讨论】:

  • 顺便说一句,有时当您发现自己担心参数类型的重载和联合时,这表明您应该改用泛型和条件类型。也就是说,重载f(a: T): U; f(a: V): W; 可以替换为f&lt;X extends T | V&gt;(a: X): X extends T ? U : W; 之类的东西,并且您可以免费获得工会的电话。但这条路线可能会让你更困惑,所以除非你真的需要,否则我不会详细说明。
  • 请详细说明!我很想在这里了解更多信息。

标签: typescript typescript-generics


【解决方案1】:

这是一个单独的答案,因为它提出了一种不同的方法。人们有时遇到重载签名的问题之一是他们don't always behave intuitively with unions of parameters。这是一个愚蠢的例子:

// call signatures
function foo(x: string): number;
function foo(x: number): string;
// implementation
function foo(x: string | number): number | string {
  return (typeof x === 'string') ? x.length : "a".repeat(x);
}

函数foo() 接受string 并返回number,或者接受number 并返回string。这按预期工作:

const num: number = foo("string"); // okay
const str: string = foo(12345); // okay

但是人们希望你可以传递string | number 类型的东西并得到number | string 类型的值。有时这种期望来自于将实现签名与调用签名混淆,有时编译器似乎应该能够选择多个重载并执行它们的统一。但这不会发生。编译器只选择一个重载签名(至少从 TS3.3 开始,无论如何。过去不可能调用函数类型的联合,但是 now you can... 好吧,使用 caveats。也许最终重载统一会发生):

const oops: string | number = foo(Math.random() < 0.5 ? "string" : 12345); // error!

我的另一个答案中的建议是通过添加一个专门对应于联合的调用签名来解决这个问题。这确实有效:

function foo(x: string): number;
function foo(x: number): string;
function foo(x: string | number): number | string; // added
function foo(x: string | number): number | string {
  return (typeof x === 'string') ? x.length : "a".repeat(x);
}

const num: number = foo("string"); // okay
const str: string = foo(12345); // okay
const oops: string | number = foo(Math.random() < 0.5 ? "string" : 12345); // okay

但还有另一种方法。您可以使用generic functionsconditional types 来代替三个调用签名,如下所示:

function foo<X extends string | number>(x: X): X extends string ? number : string;
function foo(x: string | number): number | string {
  return (typeof x === 'string') ? x.length : "a".repeat(x);
}

const num: number = foo("string"); // okay
const str: string = foo(12345); // okay
const oops: string | number = foo(Math.random() < 0.5 ? "string" : 12345); // okay

为什么会这样?

嗯,泛型类型X 将根据传入的参数推断为string | number 的任何子类型。对于foo("string")X 被推断为string literal 类型"string"。对于foo(12345)X 被推断为numeric literal 类型12345。在与Math.random() 的通话中,X 被推断为"string" | 12345。所以所有的调用都应该成功。

他们返回什么?这就是条件类型的用武之地。X extends string ? number : string 类型意味着如果Xstring 的子类型,那么条件类型将是number。否则条件类型将为string。所以对于foo("string")X extends string 为真,返回类型为number。对于foo(12345)X extends string 为假,返回类型为string。那么Math.random() 的联合类型呢?好吧,因为条件类型distribute over unions,它最终会变成number | string

你可能想也可能不想用你的函数做类似的事情:

type MaybeIterable<T> = AsyncIterable<T> | Iterable<T>;
type UnmaybeIterable<M extends MaybeIterable<any>> = M extends Iterable<infer T> ? Iterable<T> : M extends AsyncIterable<infer T> ? AsyncIterable<T> : never;
type CurriedBufferResult = {
  <M extends MaybeIterable<any>>(curriedIterable: M): UnmaybeIterable<M>
 };
export function buffer(
  size: number
): CurriedBufferResult;
export function buffer<M extends MaybeIterable<any>>(size: number, iterable: M): UnmaybeIterable<M>;
export function buffer(size: number, iterable?: MaybeIterable<any>): CurriedBufferResult | UnmaybeIterable<any>
{
  // impl here
  return null!;
}

这就是你想要的吗?不确定。

【讨论】:

【解决方案2】:

TypeScript 中的函数overloads 将函数签名分为两部分:一是call 签名列表,函数调用者可以看到。这些也被称为“重载签名”。可以有一个或多个。调用签名没有正文。

另一面是实现签名,由函数的实现看到,而不是调用者。只能有一个实现签名。实现签名必须有一个主体。

调用签名必须在实现签名之前。实现签名必须与调用签名“兼容”(例如,实现签名不能要求任何调用签名未提供的参数),但它们不是一回事。


您的问题:您试图将实现签名视为调用签名。

修复:在列表末尾添加一个额外的调用签名。可以和实现签名一样:

// call signatures:
function foobar<T>(foo: AsyncIterable<T>): AsyncIterable<T>;
function foobar<T>(foo: Iterable<T>): Iterable<T>;
// add the following call signature
function foobar<T>(foo: Iterable<T> | AsyncIterable<T>): Iterable<T> | AsyncIterable<T>;

// implementation signature:
function foobar<T>(foo: Iterable<T> | AsyncIterable<T>) {
  return foo
}

希望对您有所帮助。祝你好运!


更新以处理新形式:

type CurriedBufferResult<T> = {
  (curriedIterable: AsyncIterable<T>): AsyncIterableIterator<T>
  (curriedIterable: Iterable<T>): IterableIterator<T>
  (curriedIterable: Iterable<T> | AsyncIterable<T>): Iterable<T> | AsyncIterable<T>
};
export function buffer<T>(
  size: number
): CurriedBufferResult<T>;
export function buffer<T>(size: number, iterable: AsyncIterable<T>): AsyncIterableIterator<T>
export function buffer<T>(size: number, iterable: Iterable<T>): IterableIterator<T>
export function buffer<T>(size: number, iterable: Iterable<T> | AsyncIterable<T>): Iterable<T> | AsyncIterable<T>
export function buffer<T>(size: number, iterable?: Iterable<T> | AsyncIterable<T>):
Iterable<T> | AsyncIterable<T> | CurriedBufferResult<T> 
{
  // impl here
  return null!;
}

这与之前的解释相同,但我明确地注释了实现签名返回类型以表明它将包含调用签名的可能返回类型的意图。这是确保调用签名和实现签名兼容的一部分。

现在由您来确保实现(在// impl here 中)符合该注释。您可能看到的问题是您的函数实现实际上并没有返回上面注释的类型,并且推断的实现返回类型与调用签名不匹配。

再次祝你好运。

【讨论】:

  • 它确实解决了我的简化示例,但由于某种原因不能解决完整示例。我会更新我的帖子以包含所有内容。
猜你喜欢
  • 2016-01-07
  • 2023-03-10
  • 2016-04-26
  • 2016-03-30
  • 1970-01-01
  • 1970-01-01
  • 2017-12-30
  • 2021-12-01
  • 2022-01-15
相关资源
最近更新 更多