如何告诉 TypeScript 两个泛型类型相同?

How to tell TypeScript that two generic types are the same?

考虑以下重载函数。

function scan<A>(this: A[], f: (a: A, x: A) => A): A[];
function scan<A, B>(this: A[], f: (a: B, x: A) => B, init?: B): B[] {
    if (init === undefined) {
        const result = [this[0]];
        for (let i = 1; i < this.length; i++) {
            result.push(f(result[i - 1], this[i]));
        }
        return result;
    }

    const result = [init];
    for (let i = 0; i < this.length; i++) {
        result.push(f(result[i], this[i]));
    }
    return result;
}

注意当没有提供init时,泛型B应该和A一样。我如何将此告诉 TypeScript?目前,TypeScript 抱怨 A 不可分配给 B,反之亦然。

我能够通过削弱实现签名来进行类型检查。经验教训,实现签名必须始终是重载签名的组合。

const SCAN_EMPTY_NO_INIT = 'Scan of empty array with no initial value';

type Reducer<A, B> = (acc: B, a: A) => B;

function scan<A>(this: A[], f: Reducer<A, A>): A[];
function scan<A, B>(this: A[], f: Reducer<A, B>, init: B): B[];
function scan<A, B>(this: A[], f: Reducer<A, A | B>, init?: B): (A | B)[] {
    const { length } = this;
    let i = 0, result = [typeof init === 'undefined' ? this[i++] : init];
    if (length < i) throw new TypeError(SCAN_EMPTY_NO_INIT);
    const j = i;

    while (i < length) {
        result.push(f(result[i - j], this[i]));
        i++;
    }

    return result;
}

请注意,无法使用实现签名调用该函数。因此,您不能以意外的方式使用此函数来创建 AB 的数组。

一个 overloaded function 有一组 调用签名声明 决定如何调用该函数,并且(假设该函数已实现而不仅仅是声明)一个单个 实现 。实现签名不可调用


在您的示例代码中,您有一个调用签名

// call signature
function scan<A>(this: A[], f: (a: A, x: A) => A): A[];

和实现

// implementation
function scan<A, B>(this: A[], f: (a: B, x: A) => B, init?: B): B[] {
    /* snip */
}

但这似乎不是你想要的。您确实希望这些签名都是调用签名,如下所示:

// call signatutes
function scan<A>(this: A[], f: (a: A, x: A) => A): A[];
function scan<A, B>(this: A[], f: (a: B, x: A) => B, init?: B): B[];

// implementation
function scan(...) {

所以问题是:实现签名应该是什么?


TypeScript 的编译器无法通过分别检查每个调用签名来检查实现。有人建议在所有调用签名的 return 类型中的 microsoft/TypeScript#13235, but it was closed as too complex to implement. Instead, what the compiler does is make sure that the implementation signature parameters can handle the parameters from each of the call signatures, and make sure that the implementation signature return type can handle the return return types from each of the call signatures. That is, the return type can be the union 执行此操作。这不是类型安全的(因为您可能 return 为特定的调用签名输入了错误的类型),但是很方便。

无论好坏,这种松散的检查是 TypeScript 重载实现的工作方式。所以你在写重载函数的时候需要小心。


无论如何,这意味着实现需要像这样:

// implementation signature
function scan<A, B>(this: A[], f: (a: B | A, x: A) => A, init?: B | A) {
    if (init === undefined) {
        const result = [this[0]];
        for (let i = 1; i < this.length; i++) {
            result.push(f(result[i - 1], this[i]));
        }
        return result;
    }

    const result = [init];
    for (let i = 0; i < this.length; i++) {
        result.push(f(result[i], this[i]));
    }
    return result;
}

它无论如何都不完美,但如果我们想将这两个独立的行为放入一个重载函数中,它可能是我们能得到的最好的。

Playground link to code