【问题标题】:TypeScript infers a more general type at static implementationTypeScript 在静态实现中推断出更通用的类型
【发布时间】:2018-08-09 22:40:45
【问题描述】:

我在 TypeScript 中偶然发现了一个奇怪的情况,代码如下:

function staticImplements<T>() {
    return (constructor: T) => {};
}

enum FruitList {
    APPLE,
    BANANA,
    PEAR,
}

interface FruitInterface {
    fruit: FruitList;
}

abstract class Fruit implements FruitInterface {
    fruit: FruitList;

    constructor(fruit: FruitList) {
        this.fruit = fruit;
    }
}

interface AppleConstructor {
    new(fruit: FruitList.APPLE): AppleInterface;
}

interface AppleInterface extends Fruit {
    fruit: FruitList.APPLE;
}

class Apple extends Fruit implements AppleInterface {
    fruit: FruitList.APPLE;

    constructor(fruit: FruitList) {
        super(fruit);
    }
}
staticImplements<AppleConstructor>()(Apple);

如您所见,Fruit 的构造函数需要FruitList 类型的参数fruit,子类Apple 的构造函数也是如此,但是AppleInterface 的字段fruit 只需要枚举FruitList 的值APPLE,而不是枚举与其父FruitInterface 一样拥有的所有可能值。 AppleConstructor 也是如此,它期望参数fruit 的类型为FruitList.APPLE,用于检查Apple 静态是否实现了最后一行带有函数staticImplements 的接口。问题是,TypeScript 声明它有但它没有,这怎么可能?

【问题讨论】:

    标签: typescript oop generics constructor


    【解决方案1】:

    您的基本问题是 TypeScript 类型系统有些不健全(因此您可以编写一些非类型安全的代码)。健全性是 TypeScript 的 not a goal,尽管如果这个错误很常见,如果你打开 issue in GitHub,他们可能有兴趣解决它。我找不到适合您确切问题的解决方案。

    这里特别的不健全与不执行type variance有关。简而言之,子类型的属性reads可以是covariant(子类可以缩小其只读属性),但属性writes只能是逆变(子类应该加宽它们的只写属性)。如果一个属性既可以读取又可以写入,那么它必须是不变量才能正确。

    TypeScript 允许子类属性是协变的。这意味着当您读取属性时通常情况良好,但有时在您编写属性时可能会发生不好的事情。

    让我用更少的代码重申这里的主要问题:

    interface A {
      x: string | number
    }
    interface B extends A {
      x: number
    }
    const b: B = {x: 0};
    const a: A = b;
    a.x = "whoops"; // no error
    b.x; // number at compile time, but string at runtime
    b.x.toFixed(); // works at compile time, error at runtime
    

    看看B 是如何被认为是A 的子类型,这很好,除非你尝试将错误的东西写入它的属性。人们往往不这样做,所以语言维护者不理会它,因为防止这个问题是困难的并且非常有限(你真的想要只写属性吗?)。

    在您的情况下,您的子类正在调用超类的构造函数方法来编写一个(更广泛的)属性,即使该属性应该被子类缩小了。这是同一个问题。


    因此,这是解决您的特定问题的一种可能方法:使用泛型来指定您的实际约束,以便缩小/扩大仅在您期望的地方发生:

    interface FruitInterface<T extends FruitList> {
      fruit: T;
    }
    
    abstract class Fruit<T extends FruitList = FruitList> implements FruitInterface<T> {
      fruit: T;
    
      constructor(fruit: T) {
          this.fruit = fruit;
      }
    }
    
    interface AppleConstructor {
      new(fruit: FruitList.APPLE): AppleInterface;
    }
    
    interface AppleInterface extends Fruit<FruitList.APPLE> {
    }
    
    class Apple extends Fruit<FruitList.APPLE> implements AppleInterface {
      constructor(fruit: FruitList) {
          super(fruit); // now error as you expect
      }
    }
    

    要修复上述错误,您应该将构造函数更改为仅采用FruitList.APPLE

    希望有所帮助;祝你好运!

    【讨论】:

    • 将您的答案标记为解决答案,因为它还附带一个修复程序。答案也更详细。谢谢@jcalz!
    猜你喜欢
    • 2019-04-24
    • 1970-01-01
    • 2021-02-21
    • 1970-01-01
    • 2010-09-29
    • 2012-11-26
    • 1970-01-01
    • 2018-07-03
    • 1970-01-01
    相关资源
    最近更新 更多