代码之家  ›  专栏  ›  技术社区  ›  Wilco Bakker

TypeScript在静态实现中推断出一种更通用的类型

  •  3
  • Wilco Bakker  · 技术社区  · 7 年前

    我在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 需要参数 fruit 的类型 FruitList 子类的构造函数也是如此 Apple 然而,该领域 水果 属于 AppleInterface 只需要值 APPLE 枚举的 水果列表 枚举不像其父枚举那样保存所有可能的值 FruitInterface . 这同样适用于 AppleConstructor 需要参数 水果 类型为 FruitList.APPLE 用于检查 苹果 static实现与函数的接口 staticImplements 在最后一行。问题是,TypeScript声明它可以,但它不能,这怎么可能呢?

    1 回复  |  直到 7 年前
        1
  •  3
  •   jcalz    7 年前

    您的基本问题是TypeScript类型系统有点不健全(因此您可以编写一些非类型安全的代码)。稳健性为 not a goal 虽然如果此错误很常见,但如果您打开 issue in GitHub . 我找不到解决你问题的方法。

    这里的特别不健全与不强制执行有关 type variance . 简而言之,财产 读取 的子类型可以是 协变的 (子类可以缩小其只读属性),但属性 写入 只能是 逆变的 (子类应 拓宽 其仅写属性)。如果属性既要读也要写,则必须 不变的 保持健康。

    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 .

    希望有帮助;祝你好运