当两个参数具有相同的泛型类型时,递归约束默认值会导致在没有第一个参数的情况下错误地推断第二个参数

问题描述 投票:0回答:1

我正在使用 TypeScript 的通用约束对递归二叉搜索树 (BST) 数据类型进行建模。要定义递归泛型变量

T
的默认值,而不在默认值中使用
T
(不允许),我有一个约束的共同递归“基本情况”,如下所示:

// Bounded polymorphism using recursive generic constraints.
type IBSTBoRecDflt<K, V> = IBSTBoRec<K, V, IBSTBoRecDflt<K, V>>;

interface IBSTBoRec<K, V, T extends IBSTBoRec<K, V, T> = IBSTBoRecDflt<K, V>> {
    key: K;
    value: V;
    left?: T;
    right?: T;
}

type BSTBoRecDeflt<K, V> = BSTBoRec<K, V, BSTBoRecDeflt<K, V>>;

class BSTBoRec<K, V, T extends BSTBoRec<K, V, T> = BSTBoRecDeflt<K, V>> implements IBSTBoRec<K, V, T> {
    key: K;
    value: V;
    left?: T;
    right?: T;

    constructor(key: K, value: V, left?: T, right?: T) { 
        this.key = key;
        this.value = value;
        this.left = left;
        this.right = right;    
    }
}

const t = new BSTBoRec(5, 'e', undefined, new BSTBoRec(8, 'h'));

问题在于,当左分支丢失时,右分支

(8, h)
的类型似乎会被错误地推断出来。最后一行的
new BSTBoRec(8, 'h')
会抛出以下错误:

Argument of type 'BSTBoRec<number, string, BSTBoRec<number, string, undefined>>' is not assignable to parameter of type 
                 'BSTBoRec<number, string, BSTBoRec<number, string, BSTBoRec<number, string, undefined>>>'.
  Type 'BSTBoRec<number, string, undefined>' is not assignable to type 
       'BSTBoRec<number, string, BSTBoRec<number, string, undefined>>'.
    Type 'undefined' is not assignable to type 'BSTBoRec<number, string, undefined>'.

如果我手动指定

K
分支的
V
(8, h)
类型,错误就会消失:

const t = new BSTBoRec(5, 'e', undefined, new BSTBoRec<number, string>(8, 'h'));

为什么当省略左分支时,右分支的类型推断不正确?

这是一个带有代码和错误的playground

typescript
1个回答
0
投票

我无法准确地说为什么推理失败,因为我还没有逐步完成编译器代码。我在 GitHub 中发现的最接近的问题是 microsoft/TypeScript#45286,其中推断出无效的 generic 类型参数,即使它无法满足 constraint。这也发生在这里;看起来 T

 的第一个推理候选项未定义(这是 
left
 的类型),这会影响 
new BSTBoRec(8, 'h')上下文类型
 以包含 
BSTBoRec<number, string, undefined>
,这是不应该发生的。理想情况下,推理在这里会“失败”,并且“
T”会回落到约束条件,你只会得到“BTSBoRec<number, string>
”,但显然这不会发生。我想说这是一个 TypeScript 错误或设计限制,但谁知道它是否会得到解决。
如果我想解决这个问题,我可能会通过更改 

T
在构造函数参数类型中出现的位置来指导推理。在不做太多改变的情况下做到这一点的一种方法是:

constructor(key: K, value: V, left?: BSTBoRec<K, V, T>, right?: BSTBoRec<K, V, T>) {
    this.key = key;
    this.value = value;
    this.left = left as T;
    this.right = right as T;
}]

这会降低
T
的推理优先级,足以使其工作(除非您需要

类型断言

将其分配给
T,因为从技术上讲它可能无法分配):
const t1 = new BSTBoRec(5, 'e', new BSTBoRec(8, 'h'), undefined);
// const t1: BSTBoRec<number, string, BSTBoRecDeflt<number, string>>
const t2 = new BSTBoRec(5, 'e', undefined, new BSTBoRec(8, 'h'));
// const t2: BSTBoRec<number, string, BSTBoRecDeflt<number, string>>

或者您可以通过使用
tuple 类型的 
union

作为剩余参数来使构造函数重载或等效于重载 constructor(key: K, value: V, ...[left, right]: [undefined, T] | [T?, T?]) { this.key = key; this.value = value; this.left = left as T; this.right = right as T; }

这使得
undefined
因为

left

 更喜欢 
right
 作为推断 
T
 的地方:
const t1 = new BSTBoRec(5, 'e', new BSTBoRec(8, 'h'), undefined);
// const t1: BSTBoRec<number, string, BSTBoRec<number, string, 
//   BSTBoRec<number, string, BSTBoRecDeflt<number, string>> | undefined>> ???!

const t2 = new BSTBoRec(5, 'e', undefined, new BSTBoRec(8, 'h'));
// const t2: BSTBoRec<number, string, BSTBoRec<number, string, BSTBoRecDeflt<number, string>>>

但这仍然有一个奇怪的
undefined
类型,不应该在那里,所以我可能更喜欢顶部的那个。

总而言之,我想说,面对复杂的递归

泛型类型参数默认值
的推理可能很容易表现得很奇怪,所以你必须小心。

Playground 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.