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

Posted

技术标签:

【中文标题】如何告诉 TypeScript 两个泛型类型是相同的?【英文标题】:How to tell TypeScript that two generic types are the same? 【发布时间】:2021-08-16 07:09:11 【问题描述】:

考虑以下重载函数。

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,反之亦然。

【问题讨论】:

【参考方案1】:

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 的编译器无法通过分别检查每个调用签名来检查实现。 microsoft/TypeScript#13235 有人建议这样做,但由于太复杂而无法实施,因此被关闭。相反,编译器所做的是确保实现签名 parameters 可以处理来自每个调用签名的参数,并确保实现签名 return type 可以处理每个调用签名的返回返回类型。也就是说,返回类型可以是所有调用签名的返回类型中的union。这不是类型安全的(因为您可能会为特定调用签名返回错误的类型),但很方便。

无论好坏,这种松散的检查是 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

【讨论】:

【参考方案2】:

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

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;

请注意,不能使用实现签名调用该函数。因此,您不能以非预期的方式使用此函数来创建As 或Bs 的数组。

【讨论】:

以上是关于如何告诉 TypeScript 两个泛型类型是相同的?的主要内容,如果未能解决你的问题,请参考以下文章

Typescript中的类型和泛型

如何使用泛型缩小 TypeScript 联合类型

TypeScript,啥是对象文字的调用签名以及它们如何与泛型类型一起使用?

Typescript - 如何禁止两个解析为相同类型的类型别名互换使用?

TypeScript - 如何将索引签名表示为泛型类型

使用泛型制作 Array.flat(1) 的 Typescript 包装器