【问题标题】:Why was `Subject & Array<Value>` necessary for a compatible overload signature?为什么兼容的重载签名需要 `Subject & Array<Value>`?
【发布时间】:2019-01-11 23:17:07
【问题描述】:

我得到:

重载签名与函数 implementation.ts(2394) 不兼容

开启:

/** Iterate through an Array. */
export default function eachr<Value>(
    subject: Array<Value>,
    callback: IteratorCallback<typeof subject, number, Value>
): typeof subject

整个sn-p:

export interface IteratorCallback<Subject, Key, Value> {
    (this: Subject, value: Value, key: Key, subject: Subject): void | boolean
}

/** Iterate through an Array. */
export default function eachr<Value>(
    subject: Array<Value>,
    callback: IteratorCallback<typeof subject, number, Value>
): typeof subject

/** Iterate through an Object. */
export default function eachr<RecordKey extends keyof any, Value>(
    subject: Record<RecordKey, Value>,
    callback: IteratorCallback<typeof subject, RecordKey, Value>
): typeof subject

/** Iterate through the subject. */
export default function eachr<RecordKey extends keyof any, Value>(
    input: Array<Value> | Record<RecordKey, Value>,
    callback: IteratorCallback<typeof input, RecordKey | number, Value>
): typeof input {
    if (Array.isArray(input)) {
        // Array
        const subject = input as Array<Value>
        for (let key = 0; key < subject.length; ++key) {
            const value = subject[key]
            if (callback.call(subject, value, key, subject) === false) {
                break
            }
        }
    } else {
        // Object
        const subject = input as Record<RecordKey, Value>
        for (const key in subject) {
            if (subject.hasOwnProperty(key)) {
                const value = subject[key]
                if (callback.call(subject, value, key, subject) === false) {
                    break
                }
            }
        }
    }

    // Return
    return input
}

我可以通过将其更改为:

/** Iterate through an Array. */
export default function eachr<Subject extends Array<Value>, Value>(
    subject: Subject & Array<Value>,
    callback: IteratorCallback<typeof subject, number, Value>
): typeof subject

但是,我不明白为什么要修复它。问题出在哪里,为什么这种改变会让问题消失?

更令我惊讶的是,如果我将相同的更改应用于纯对象迭代器函数,它会导致它失败:

/** Iterate through an Object. */
export default function eachrObject<
    Subject extends Record<RecordKey, Value>,
    RecordKey extends keyof any,
    Value
>(
    subject: Subject & Record<RecordKey, Value>,
    callback: IteratorCallback<typeof subject, RecordKey, Value>
): typeof subject {
    for (const key in subject) {
        if (subject.hasOwnProperty(key)) {
            const value = subject[key]
            // above fails with: Element implicitly has an 'any' type because type 'Record<RecordKey, Value>' has no index signature.ts(7017)
            // below fails with: Argument of type 'string' is not assignable to parameter of type 'RecordKey'.ts(2345)
            if (callback.call(subject, value, key, subject) === false) {
                break
            }
        }
    }
    return subject
}

而这有效:

/** Iterate through an Object. */
export default function eachrObject<RecordKey extends keyof any, Value>(
    subject: Record<RecordKey, Value>,
    callback: IteratorCallback<typeof subject, RecordKey, Value>
): typeof subject {
    for (const key in subject) {
        if (subject.hasOwnProperty(key)) {
            const value = subject[key]
            if (callback.call(subject, value, key, subject) === false) {
                break
            }
        }
    }
    return subject
}

然而,这两种形式都适用于 Array 迭代器:

/** Iterate through an Array. */
export default function eachrArray<Subject extends Array<Value>, Value>(
    subject: Subject & Array<Value>,
    callback: IteratorCallback<typeof subject, number, Value>
): typeof subject {
    for (let key = 0; key < subject.length; ++key) {
        const value = subject[key]
        if (callback.call(subject, value, key, subject) === false) {
            break
        }
    }
    return subject
}

/** Iterate through an Array. */
export default function eachrArray<Value>(
    subject: Array<Value>,
    callback: IteratorCallback<typeof subject, number, Value>
): typeof subject {
    for (let key = 0; key < subject.length; ++key) {
        const value = subject[key]
        if (callback.call(subject, value, key, subject) === false) {
            break
        }
    }
    return subject
}

那么为什么更改为Subject extends Array&lt;Value&gt; 是重载区域迭代器兼容性所必需的,而Subject extends Record&lt;RecordKey, Value&gt; 却破坏了对象迭代器?

抱歉,这里的代码量太少了,这是我可以将其归结为的最小用例,其中包含所有考虑因素。

【问题讨论】:

    标签: typescript


    【解决方案1】:

    老实说,这要经历很多事情,而且我想我无法准确回答您为什么要让事情正常进行。在我看来,您的重载签名都应该失败。我们来看一个超级简单的重载/实现示例:

    function foo(x: string): void; // narrower, okay 
    function foo(x: string | number | boolean): void; // wider, error
    function foo(x: string | number): void {} // impl
    

    注意第二个重载签名如何给出它与实现签名不兼容的错误。这是因为重载的x 是一个比实现的x 更宽的类型。并且重载需要更窄的类型。

    还要注意通常(因为--strictFunctionTypes was introduced in TypeScript 2.6)函数类型在其参数类型中是逆变的。这会导致以下行为:

    type StringAccepter = (x: string) => void;
    const helloAccepter: StringAccepter = (x: "hello") => {}; // error
    const stringOrNumberAccepter: StringAccepter = (x: string | number) => {}; // okay
    

    helloAccepter 不是有效的StringAccepter,因为"hello"string 窄,而stringOrNumberAccepter 一个有效的StringAccepter 因为string | number 比@987654334 宽@。因此函数参数变宽会使它们的函数变窄,反之亦然:

    function bar(cb: (x: "hello")=>void): void; // error, cb is too wide because x is too narrow
    function bar(cb: (x: string | number)=>void): void; // okay, cb is narrower because x is wider 
    function bar(cb: StringAccepter): void {} // impl
    

    所以我希望你的两个重载都会失败,因为实现签名的 callback 类型 (IteratorCallback&lt;typeof input, RecordKey | number, Value&gt;) 实际上比你的任何一个调用签名的 callback 类型更窄。

    在这一点上,不要试图通过涉及额外 Subject 类型参数的可能解决方案并理解为什么有些事情有效而有些事情不(这让我的大脑受伤......也许有一个编译器错误) ?也许不是?谁知道),我将采用我建议的解决方案...使实现签名真正宽到足以支持两个调用签名:

    /** Iterate through an Array. */
    export function eachr<Value>(
      subject: Array<Value>,
      callback: IteratorCallback<typeof subject, number, Value>
    ): typeof subject
    
    /** Iterate through an Object. */
    export function eachr<RecordKey extends keyof any, Value>(
      subject: Record<RecordKey, Value>,
      callback: IteratorCallback<typeof subject, RecordKey, Value>
    ): typeof subject
    
    /** Iterate through the subject. */
    export function eachr<RecordKey extends keyof any, Value>(
      input: Array<Value> | Record<RecordKey, Value>, 
      // here is the change
      callback: IteratorCallback<Array<Value>, number, Value> |
        IteratorCallback<Record<RecordKey, Value>, RecordKey, Value>
    ): typeof input {
      if (Array.isArray(input)) {
        // Array
        const subject = input as Array<Value>
        // a new assertion:
        const cb = callback as IteratorCallback<Array<Value>, number, Value>;
        for (let key = 0; key < subject.length; ++key) {
          const value = subject[key]
          if (cb.call(subject, value, key, subject) === false) {
            break
          }
        }
      } else {
        // Object
        const subject = input as Record<RecordKey, Value>
        // a new assertion:
        const cb = callback as IteratorCallback<Record<RecordKey, Value>, RecordKey, Value>;
        for (const key in subject) {
          if (subject.hasOwnProperty(key)) {
            const value = subject[key]
            if (cb.call(subject, value, key, subject) === false) {
              break
            }
          }
        }
      }
    
      // Return
      return input
    }
    

    不同之处在于实现签名上的callback 参数是每个调用签名上callback 参数的类似类型的真正联合。此外,实现本身需要对callbackcb 进行缩小断言,其方式与您已经为inputsubject 所做的断言大致相同。

    现在编译器应该很高兴。希望有帮助;祝你好运!

    【讨论】:

    • input 上的类型断言首先是不必要的……我得出了同样的结论,即两个重载都不应该起作用。非常令人困惑的行为。
    • 关于input 上不必要的类型断言的好点;控制流分析应该处理这种缩小。
    【解决方案2】:

    问题

    这与联合类型的工作方式有关。问题源于上次(累积)过载:

    callback: IteratorCallback<typeof input, RecordKey | number, Value>
    

    因为这里的inputArray&lt;Value&gt; | Record&lt;RecordKey, Value&gt; 类型,所以以这种方式构造的callback 的定义允许存在4 种可能的组合:

    IteratorCallback<Array<Value>, RecordKey, Value>
    IteratorCallback<Array<Value>, number, Value>
    IteratorCallback<Record<RecordKey, Value>, RecordKey, Value>
    IteratorCallback<Record<RecordKey, Value>, number, Value>
    

    但根据您之前的重载定义,其中只有 2 个是有效的

    解决方案

    这可以通过说callback 是以下两种类型之一来解决:

    callback: IteratorCallback<Array<Value>, number, Value> | IteratorCallback<Record<RecordKey, Value>, RecordKey, Value> 
    

    这会处理 Overload signature is not compatible with function implementation 错误。 然而,另一个问题已经被发现:TypeScript 没有在提供的input 的类型和随之而来的callback 之间建立联系。因为最后一个重载仍然使用联合类型——两个用于input,两个用于callback——TypeScript 认为可能会发生 4 种情况。似乎这个问题最流行的解决方法就是使用类型断言。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-06-18
      • 1970-01-01
      • 1970-01-01
      • 2018-06-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多