【问题标题】:Why can't a generic member function in a class implementing an interface take an argument of the type of the class (instead of the interface)?为什么实现接口的类中的泛型成员函数不能接受类类型(而不是接口)的参数?
【发布时间】:2021-10-11 08:16:30
【问题描述】:

考虑使用likes<T extends IDog>( other: T ) 方法的接口IDog。该方法接受一个类型扩展接口的参数。为什么不允许在派生类Dog 中使用类作为参数类型而不是接口来实现该方法?

interface IDog
{
    likes<T extends IDog>( other: T ): boolean;
}

class Dog implements IDog
{
    private name = "Buddy";
    public likes<T extends Dog>( other: T )
        // ^^^^^
        // error: Property 'likes' in type 'Dog' is not 
        // assignable to the same property in base type 'IDog' [...]
        // Property 'name' is missing in type 'IDog' but required in type 'Dog'
    {
        return true;
    }
}

删除私有属性name 会使错误消失,但不是解决我现实世界问题的方法。奇怪的是,没有泛型的同一个例子工作得很好:

interface ICat
{
    likes( other: ICat ): boolean;
}

class Cat implements ICat
{
    private name = "Simba";
    public likes( other: Cat )  // no error using Cat here (instead of ICat)
    {
        return true;
    }
}

我在这里错过了什么?

【问题讨论】:

    标签: typescript typescript-types


    【解决方案1】:

    假设编译器对您实现IDog 的方式没有任何问题。那么以下就可以了:

    class Dog implements IDog {
      private name = "Buddy";
      public likes<T extends Dog>(other: T) {
        return other.name.toUpperCase() === "FRIEND";
      }
    }
    
    const myDog: IDog = new Dog(); // should be okay if Dog implements IDog
    

    但这可能会导致编译器无法捕获的运行时错误:

    const eyeDog: IDog = {
      likes(other) {
        return true;
      }
    }
    console.log(myDog.likes(eyeDog)) // okay for the compiler, but RUNTIME ERROR
    

    所以编译器认为Dog 没有正确实现IDog 是正确的。允许这样做是"unsound"。如果你有一个想要扩展的函数类型(更具体),你不能让它的参数更具体和更健全;你需要让它们更通用。这意味着应该检查函数参数contravariantly(也就是说,它们与函数类型相反......它们反变化......逆变)。


    当然,这会导致您对Cat 产生疑问。完全相同的论点在那里不起作用吗?

    class Cat implements ICat {
      private name = "Simba";
      public likes(other: Cat) { // no error
        return other.name.toUpperCase() === "FRIEND";
      }
    }
    const myCat: ICat = new Cat(); // no error
    
    const eyeCat: ICat = {
      likes(other) { return true; }
    }
    
    console.log(myCat.likes(eyeCat)) // no compiler error, but RUNTIME ERROR
    

    确实如此!编译器允许使用 CatICat 进行不合理的扩展。什么给了?

    这是明确的故意行为;方法parameters are checked bivariantly,这意味着编译器将接受更广泛的参数类型(安全)和更窄的参数类型(不安全)。这显然是因为,在实践中,人们很少使用myCat(或myDog)编写上面那种不安全的代码,而这种不安全正是允许存在许多有用的类型层次结构的原因(例如,TypeScript 允许Array&lt;string&gt;Array&lt;string | number&gt; 的子类型。


    那么,等等,为什么编译器关心泛型类型参数的可靠性而不关心方法参数?好问题;我不知道对此有任何“官方”答案(尽管我可能会查看 GitHub 问题以查看 TS 团队中是否有人对此发表过评论)。一般来说,TypeScript 中的健全性违规是根据启发式和实际代码仔细考虑的。

    我的猜测是人们通常希望他们的泛型具有类型安全性(正如microsoft/TypeScript#16368 对它们的更严格检查的实现所证明的那样),特别是添加额外的代码以允许方法参数双变量会比它的价值更麻烦。

    您可以通过启用the --noStrictGenericChecks compiler option 来禁用泛型的严格性检查,但我不建议故意降低编译器的类型安全性,因为它的影响远不止Dog 问题,而且很难找到资源当您依赖不寻常的编译器标志时寻求帮助。


    请注意,您可能正在寻找每个子类或实现类只能 likes() 其自己类型的参数而不是所有可能的子类型的模式。如果是这样,那么您可以考虑改用the polymorphic this type。当您使用this 作为类型时,它就像泛型类型,表示“调用此方法的子类是什么类型”。但它是专门为允许您似乎正在做的事情而设计的:

    interface IGoldfish {
      likes(other: this): boolean;
    }
    
    class Goldfish implements IGoldfish {
      private name = "Bubbles";
      public likes(other: this) {
        return other.name.toUpperCase() === "FRIEND";
      }
    }
    const myFish: IGoldfish = new Goldfish();
    

    这当然和其他两个例子有同样的问题:

    const eyeFish: IGoldfish = { likes(other) { return true; } }
    console.log(myFish.likes(eyeFish)) // RUNTIME ERROR
    

    所以它不是治疗不健全的灵丹妙药。但它与没有泛型参数警告的泛型版本非常相似。

    Playground link to code

    【讨论】:

      【解决方案2】:

      想象这样一种情况:你有

      const myDog: Dog
      const someOtherDog: IDog
      

      还有这样一个功能:

      function seeIfLikes(dog: IDog, anotherDog: IDog) {
        return dog.likes(anotherDog)
      }
      

      这个函数看起来不错,IDog.likes() 想要扩展 IDog 作为参数。

      但是当你调用 seeIfLikes(myDog, someOtherDog) 时,会发生一些意想不到的事情:myDog 被强制转换为 IDog,所以 TypeScript 会忘记它的 likes() 方法需要扩展 Dog 的东西,而不是 IDog

      因此,即使someOtherDog 实际上没有扩展Dog,并且如果您的Dog.likes() 包含一些特定于Dog 类而不是IDog 的代码,此函数调用也会通过类型检查,您会得到运行时 kaboom。

      这就是为什么我们不能在子类型中添加新的泛型参数限制的原因:它们可能被强制转换为它们的超类型,并且该限制将消失。希望这足够清楚,可以理解。


      是的,Cat 示例将遇到完全相同的问题,但 tsc 以未知原因让它通过。可能是类型系统的限制,或者是更好报告的错误。

      【讨论】:

      • 但是为什么 Cat 示例可以工作呢?你对猫做的同样的事情:const seeIfLikes = ( cat1: ICat, cat2: ICat ) =&gt; cat2.likes( cat1 );
      【解决方案3】:

      如果你想实现IDog接口,你需要确保likes方法可以赋值给IDog接口的likes方法,对吧?

      考虑这个例子:

      declare var dog: (arg: {}) => boolean
      
      declare var idog: (arg: { name: string }) => boolean
      
      dog = idog // error
      
      idog = dog // ok
      

      您无法将idog 函数分配给dog 函数,因为无论dog 实现是否允许idog 实现使用name 参数。

      你可能会想,这很奇怪,因为这是按预期工作的:

      declare var iarg: { name: string }
      declare var arg: {}
      
      iarg = arg // error, because iarg uses `name` and arg does not have this property
      arg = iarg
      

      属性多的对象可以分配给属性少的对象,很直观。

      在您的示例中,问题发生了,因为函数类型与其参数类型相反。有关更多上下文,请参阅此answer

      现在,尝试禁用strictFunctionTypes 标志。你会看到这段代码会编译:

      let dog = (arg: {}) => true
      
      let idog = (arg: { name: string }) => {
          console.log(arg.name)
          return true
      }
      
      dog = idog // ok
      

      让我们回到泛型的原始问题:

      
      let dog = <T extends {}>(arg: T) => true
      
      let idog = <T extends { name: string }>(arg: T) => true
      
      dog = idog // error
      idog = dog
      

      即使没有strictFunctionTypes 标志,此代码仍然会产生错误。尽管函数参数位置被双变量处理(没有 strictFunctionTypes),但我认为泛型仍然被逆变处理。我可能是错的,所以如果有人纠正我,我会很高兴。

      这意味着 typescript 将尝试检查 T extends {} 是否已分配给 T extends { name: string },尽管我们正在尝试将 T extends { name: string } 分配给 T extends {}

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-09-15
        • 2019-03-22
        • 1970-01-01
        • 1970-01-01
        • 2018-02-10
        • 2013-08-16
        • 2010-11-11
        • 2016-01-06
        相关资源
        最近更新 更多