为什么要使用兼容的重载签名才能使用“主题和数组<值>”?

时间:2019-01-02 14:50:19

标签: typescript

我得到:

  

重载签名与函数实现不兼容。ts(2394)

开启:

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

整个摘要中的

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<Value>进行更改才能使区域迭代器的兼容性过载,而Subject extends Record<RecordKey, Value>却破坏了对象迭代器?

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

2 个答案:

答案 0 :(得分:5)

老实说,这有很多困难要解决,我想我无法确切回答您为什么需要工作。我认为,您的重载签名都应该失败。让我们看一个超简单的重载/实现示例:

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起)函数类型的参数类型是 contravariant 。导致以下行为:

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

helloAccepter不是有效的StringAccepter,因为"hello"string窄,而stringOrNumberAccepter 是有效的{{ 1}},因为StringAccepterstring | number宽。因此,功能参数变宽会使它们的功能变窄,反之亦然:

string

因此,我希望您的两个重载都将失败,因为实现签名的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<typeof input, RecordKey | number, Value>类型都窄。

在这一点上,与其尝试尝试涉及额外的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参数类似类型的真实结合。另外,实现本身需要对callbackcallback进行缩小的断言,就像您已经对cbinput做断言一样。

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

答案 1 :(得分:2)

问题

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

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

由于input的类型为Array<Value> | Record<RecordKey, Value>,因此以这种方式构造的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> 

这可以解决重载签名与函数实现不兼容错误。 但是,还发现了另一个问题:TypeScript不会在提供的input的类型和随之而来的callback之间建立连接。由于最后一个重载仍使用联合类型-两个用于input,两个用于callback-TypeScript认为可能发生4种情况。似乎最流行的解决方法是使用类型断言。