【问题标题】:TS2322: 'T' could be instantiated with an arbitrary type which could be unrelated to 'SuperClass'TS2322:“T”可以用与“SuperClass”无关的任意类型实例化
【发布时间】:2020-11-28 23:46:04
【问题描述】:

我有一个包含大量子类的基类。

class SuperClass{
    constructor(){}
}
class SubClass1 extends SuperClass{
    constructor(){super()}
    SubClass1Prop(){}
}
class SubClass2 extends SuperClass{
    constructor(){super()}
    SubClass2Prop(){}
}

我有一个包含不同子类混合的列表,我已将其类型声明为SuperClass[],因为它们共享那个共同的祖先。

let list:SuperClass[] = [];
list.push(new SubClass1());
list.push(new SubClass2());

我有一个函数,它将子类作为参数,然后过滤列表以返回该子类的实例。

此函数的类型声明取自此 SO 答案 (Type for function that takes a class as argument, and returns an instance of that class),稍有不同之处在于我返回的是列表而不是单个对象。

错误就在这里。我控制可以将哪些类型传递给 filterList 函数,并且可以保证它始终是 SuperClass 的子级。但是,TypeScript 无法知道这一点。如何声明我传递给此函数的任何 className 必须始终是 SuperClass 的子级,并且不能是“任意类型”?

function filterList<T>(className: { new():T }):T[] {
    //TS2322: Type 'SuperClass[]' is not assignable to type 'T[]'.   
    // Type 'SuperClass' is not assignable to type 'T'.     
    // 'T' could be instantiated with an arbitrary type which could be unrelated to 'SuperClass'
    return list.filter(obj => obj instanceof className);
}

上述函数的示例使用场景。 (删除空安全检查以简化演示)

我要求在不使用“as”关键字显式类型转换的情况下,将过滤列表正确识别为具有 SubClass1[] 类型,而不是 SuperClass[] 或 any[] 类型。目前所有这些要求都满足了,但是TS错误在上面的函数中。任何解决方案都不得破坏此处的类型检查。

let filteredList = filterList(SubClass1);
filteredList[0].SubClass1Prop();

【问题讨论】:

    标签: typescript


    【解决方案1】:

    虽然简单很多,但是你可以使用InstanceType 和generic parameters 和type guards 来达到同样的效果:

    function filterList<T extends { new (...args: any): any }>(className: T): InstanceType<T>[] {
        const isT = (potentialT: any): potentialT is InstanceType<T> => {
            return potentialT instanceof className;
        }
        return list.filter<InstanceType<T>>(isT);
    }
    

    Playground

    【讨论】:

    • 我了解类型保护,我喜欢它可以直接用作过滤器回调。但是 InstanceType 对我来说是新的,并且链接的手册描述不太清楚。这是否意味着potentialT 是从类T 构造的?参考手册,为什么可以使用InstanceType&lt;T&gt;而不是InstanceType&lt;typeof T&gt;,为什么InstanceType&lt;Function&gt;无效?类在技术上不是函数吗?
    • @dreamturtle,InstanceType 指定类实例的类型(如您所说),但T 是类型,而不是值(即它已经是typeof className) ,对于 TypeParameters,您可以查看 generics。最后,TypeScript 区分函数和类(类是 TS 中函数的子集)。
    【解决方案2】:

    list 是SuperClass[] 类型,因此过滤它会返回另一个SuperClass[],因此该函数返回类型SuperClass[]。但是,根据函数的类型,它实际上返回类型T[]。如果T 满足SuperClass,例如在您调用它的示例方式中,那么快乐的日子。但是如果T 不满足SuperClass,那么您返回的过滤列表将根本无法分配给函数的实际返回类型。要解决此问题,您只需要确保T 满足SuperClass。这可以通过类型参数T extends SuperClass 轻松完成。

    您应该始终限制您的类型参数(请参阅the docs),除非它们的类型实际上并不重要,即使它没有错误。用不满足SuperClass 的类调用filterList() 根本没有意义,因此如果你尝试,你应该告诉编译器阻止你。在这种情况下,它甚至在你没有尝试的情况下就阻止了你,因为它认为它本质上是不安全的。

    您会注意到,进行该更改会导致另一个错误:

    “SuperClass”可分配给“T”类型的约束,但“T”可以用约束“SuperClass”的不同子类型来实例化。

    这里的问题和以前一样:函数返回类型SuperClass[],但它的类型是返回T[]。如果T 是 SuperClass,那么快乐的日子。但如果不是,并且T 是具有其他成员的SuperClass 的子类,那么SuperClass[] 将不能分配给T[]。

    过滤后的列表实际上是SuperClass[] 类型,这可能是违反直觉的,因为您清楚地从数组中过滤掉了不是T 类型的所有内容,这表明过滤后的数组应该是@ 类型987654347@。但它并不完全那样工作。类型保护 obj instanceof className 创建语义意味着 obj 是类型 T,因此从回调的那一点开始,编译器就会知道这一点。但是,该含义不会提供给调用该回调的人。你的Array.prototype.filter() 调用不知道什么类型满足它的回调,因为回调没有告诉它。它所知道的只是它过滤的数组是SuperClass[] 类型,因此它可以说的最好的结果是生成的数组也是SuperClass[] 类型。

    解决方案很简单:您只需要在回调中提供一个类型谓词(请参阅the docs)。由此,filter() 将知道哪些类型满足其回调,使其能够准确说出结果数组的类型。

    const foo: SuperClass[] = list.filter(obj => obj instanceof className)
    const bar: T[] = list.filter((obj): obj is T => obj instanceof className)
    

    因此,通过这两个修复,您的 filterList() 函数现在看起来像这样:

    function filterList<T extends SuperClass>(className: { new():T }):T[] {
        return list.filter((obj): obj is T => obj instanceof className);
    }
    

    一边

    我怀疑你在这里尝试做什么;抽象地说,我不确定这是一个好主意。您应该知道,类继承本身在类型方面没有语义意义。给定class A {},new A 可以分配给类型SuperClass。这不是A 实际与SuperClass 相关的问题,而是A 满足SuperClass 的问题。我认为您的 filterList 函数中没有任何方法可以实际强制 T 是 SuperClass 的子类。

    【讨论】:

    • "给定类 A {},新的 A 可以分配给类型 SuperClass。"是的,这对于这个玩具示例来说是正确的,但在实际代码中,SuperClass 将具有由所有子类继承的属性和/或方法,但在不相关的类中可能不存在。也就是说,是否存在一种方法来声明某个变量必须是某个 SuperClass 的后代,而不仅仅是具有相同的属性/方法名称?检查属性是否适合在语义上不同于检查对象的原型链中是否存在某些祖先类。
    • 正如我在最后所说的那样:我不这么认为。我以前从来没有考虑过这个问题,所以我试着为你的问题做一些研究,但我什么也找不到。我认为类继承本身根本不是类型系统中存在的概念(super() 除外);一个对象只是由它的属性定义的。这就是为什么我怀疑你在这样的类继承上投入如此之多的部分原因;我只是不确定这是否是管理对象的好方法。
    【解决方案3】:

    我必须添加一个答案,因为我发现了一些原始问题未明确询问的新信息(关于抽象类)。

    function filterList<T extends SuperClass>(className: Function & {prototype: T}): T[] {
        return list.filter((e): e is T => e instanceof className);
    }
    

    此解决方案基于 matthewlucock (https://stackoverflow.com/a/63323255/14073412) 给出的解决方案。 这里的区别是函数参数的类型是Function &amp; {prototype: T}而不是{ new():T }。

    关键问题是抽象类是不可构造的(“newable”)。如果想将抽象类传递给filterList() 并能够获取所有实现抽象类的对象,使用{ new():T } 将失败并出现错误:

    无法将抽象构造函数类型分配给非抽象构造函数类型。(2345)

    与Function 的交集对于确保传递的参数是函数而不是常规对象是必要的。没有它,您将无法使用instanceof 运算符:

    “instanceof”表达式的右侧必须是“any”类型或可分配给“Function”接口类型的类型。(2359)

    请注意,尽管此解决方案允许将抽象类传递给过滤器函数,但作为一种折衷,您在确保传递的参数实际上是一个类时会失去一些类型安全性。尝试使用非类函数调用 filterList 不会产生 TypeScript 错误:

    function NonConstructibleFunction(){}
    filterList(NonConstructibleFunction);
    

    但是,如果类属性/方法的形状不匹配,TypeScript 会因意外传入不相关的类而捕获错误。请注意,构造函数形状未检查。

    class UnrelatedDummyClass{
        constructor(differentConstructorParameters:number){}
    }
    //Property 'commonFunc' is missing in type 'UnrelatedDummyClass' but required in type 'SuperClass'.(2345)
    filterList(UnrelatedDummyClass);
    

    查看演示:Playground Link

    对我来说,这个解决方案更可取,因为在我的用例中,我可以控制传递给filterList的参数,并且很容易避免传入不可构造函数的逻辑错误。

    我不相信有可能获得两全其美,具有限制无效类/函数的参数安全性,同时还允许具有有效具体实现的抽象类。如果我对此有误,请纠正我(和/或发布您自己的答案)

    【讨论】:

      猜你喜欢
      • 2021-10-26
      • 2021-09-21
      • 1970-01-01
      • 1970-01-01
      • 2021-01-19
      • 2021-05-11
      • 2021-10-08
      • 1970-01-01
      • 2019-10-23
      相关资源
      最近更新 更多