strictFunctionTypes 限制泛型类型

Posted

技术标签:

【中文标题】strictFunctionTypes 限制泛型类型【英文标题】:strictFunctionTypes restricts generic type 【发布时间】:2020-08-25 00:21:30 【问题描述】:

问题似乎特定于 strictFunctionTypes 如何影响泛型类类型。

这里有一个类,可以密切再现发生的事情,并且由于要求不能进一步简化,any 用于指定不施加额外限制的部分(playground):

class Foo<T> 
    static manyFoo(): Foo<any[] |  [s: string]: any >;
    static manyFoo(): Foo<any[]> 
        return ['stub'] as any;
    

    barCallback!: (val: T) => void;

    constructor() 
        // get synchronously from elsewhere
        (callback => 
            this.barCallback = callback;
        )((v: any) => );
    

    baz(callback: ((val: T) => void)): void 

barCallback 签名中的T 泛型类型导致类型错误:

(method) Foo<T>.manyFoo(): Foo<any[]>
This overload signature is not compatible with its implementation signature.(2394)

只有在barCallback函数类型中将T用作val类型时才会出现问题。

如果barCallbackbaz 不使用T 作为参数类型,它就会消失:

barCallback!: (val: any) => void | T;

如果没有 manyFoo 方法重载或签名不太多样化,它就会消失。

如果barCallback 在类中有方法签名,则不会出现,但这会阻止它在以后被分配:

barCallback!(val: T): void;

在这种情况下,严格的val 类型并不重要,可以牺牲。由于barCallback不能被类中的方法签名替换,接口合并似乎是一种在不进一步放松类型的情况下抑制错误的方法:

interface Foo<T> 
  barCallback(val: T): void;

在类似情况下还有其他可能的解决方法吗?

我很感激解释为什么函数类型中的 val: T 会以这种方式影响类类型。

【问题讨论】:

为什么barCallback不能用方法签名代替? typescriptlang.org/play?#code/… 抱歉,在写这篇文章的时候丢失了一个重要的部分。是的,方法签名存在问题,因为它最初是 barCallback! 并同步设置,只是不在构造函数范围内。 【参考方案1】:

这是其核心的差异问题。所以首先是差异入门:

关于方差

给定一个泛型类型Foo&lt;T&gt;,以及两个相关类型AnimalDog extends AnimalFoo&lt;Animal&gt;Foo&lt;Dog&gt; 之间有四种可能的关系:

    协方差 - Foo&lt;Animal&gt;Foo&lt;Dog&gt; 的继承箭头指向 相同方向,就像 AnimalDog 一样,所以 Foo&lt;Dog&gt;Foo&lt;Animal&gt;,这也意味着Foo&lt;Dog&gt; 可以分配给Foo&lt;Animal&gt;
type CoVariant<T> = () => T
declare var coAnimal: CoVariant<Animal>
declare var coDog: CoVariant<Dog>
coDog = coAnimal; // ?
coAnimal = coDog; // ✅
    逆变 - Foo&lt;Animal&gt;Foo&lt;Dog&gt; 的继承箭头指向 相反方向,就像 AnimalDog 一样,所以 Foo&lt;Animal&gt; 实际上是一个子类型的Foo&lt;Dog&gt;,这也意味着Foo&lt;Animal&gt; 可分配给Foo&lt;Dog&gt;
type ContraVariant<T> = (p: T) => void
declare var contraAnimal: ContraVariant<Animal>
declare var contraDog: ContraVariant<Dog>
contraDog = contraAnimal; // ✅
contraAnimal = contraDog; // ?
    不变性 - 尽管 DogAnimal 是相关的,但 Foo&lt;Animal&gt;Foo&lt;Dog&gt; 在它们之间没有任何关系,因此两者都不能分配给另一个。
type InVariant<T> = (p: T) => T
declare var inAnimal: InVariant<Animal>
declare var inDog: InVariant<Dog>
inDog = inAnimal; // ?
inAnimal = inDog; // ?
    Bivariance - 如果DogAnimal 相关,则Foo&lt;Animal&gt; 都是Foo&lt;Dog&gt; 的子类型,Foo&lt;Animal&gt;Foo&lt;Dog&gt; 的子类型,这意味着任何一个类型都可以分配给另一个。在更严格的类型系统中,这将是一种病态的情况,T 可能实际上没有被使用,但在打字稿中,方法参数位置被认为是双变量的。

class BiVariant<T>  m(p: T): void  
declare var biAnimal: BiVariant<Animal>
declare var biDog: BiVariant<Dog>
biDog = biAnimal; // ✅
biAnimal = biDog; // ✅

All Examples - Playground Link

所以问题是T 的使用如何影响方差?在打字稿中,类型参数的位置决定了方差,一些例子:

    Co-variint - T 用作字段或函数的返回类型 Contra-variint - T 用作strictFunctionTypes 下的函数 签名的参数 不变 - T 用于协变和逆变位置 Bi-variant - T 用作strictFunctionTypes 下的方法 定义的参数,或者如果strictFunctionTypes 关闭,则用作方法或函数的参数类型。李>

strictFunctionTypes中方法和函数参数行为不同的原因解释here:

更严格的检查适用于所有函数类型,但源自方法或构造函数声明的函数类型除外。专门排除方法以确保泛型类和接口(例如 Array)继续主要以协变方式相关。严格检查方法的影响将是一个更大的突破性变化,因为大量泛型类型将变得不变(即便如此,我们可能会继续探索这种更严格的模式)。

回到问题

让我们看看T 的用法如何影响Foo 的方差。

barCallback!: (val: T) =&gt; void; - 用作函数成员中的参数 -> 反变量位置

baz(callback: ((val: T) =&gt; void)): void - 用作另一个函数的回调参数中的参数。这有点棘手,剧透警报,这将证明是协变的。让我们考虑这个简化的例子:

type FunctionWithCallback<T> = (cb: (a: T) => void) => void

// FunctionWithCallback<Dog> can be assigned to FunctionWithCallback<Animal>
let withDogCb: FunctionWithCallback<Dog> = cb=> cb(new Dog());
let aliasDogCbAsAnimalCb: FunctionWithCallback<Animal> = withDogCb; // ✅
aliasDogCbAsAnimalCb(a => a.animal) // the cb here is getting a dog at runtime, which is fine as it will only access animal members


let withAnimalCb: FunctionWithCallback<Animal> = cb => cb(new Animal());
// FunctionWithCallback<Animal> can NOT be assigned to FunctionWithCallback<Dog>
let aliasAnimalCbAsDogCb: FunctionWithCallback<Dog> = withAnimalCb; // ?
aliasAnimalCbAsDogCb(d => d.dog) // the cb here is getting an animal at runtime, which is bad, since it is using `Dog` members

Playground Link

在第一个示例中,我们传递给aliasDogCbAsAnimalCb 的回调预计会收到Animal,因此它只使用Animal 成员。实现withDogCb 将创建一个Dog 并将其传递给回调,但这很好。仅使用它所期望的基类属性,回调将按预期工作。

在第二个示例中,我们传递给aliasAnimalCbAsDogCb 的回调预计会收到Dog,因此它使用Dog 成员。但是实现withAnimalCb 将把一个动物的实例传递给回调函数。这可能会导致运行时错误,因为回调最终会使用不存在的成员。

因此,仅将FunctionWithCallback&lt;Dog&gt; 分配给FunctionWithCallback&lt;Animal&gt; 是安全的,我们得出这样的结论:T 的这种用法决定了协方差。

结论

所以我们在Foo 的协变和逆变位置都使用了T,这意味着FooT 中是不变的。这意味着就类型系统而言,Foo&lt;any[] | [s: string]: any &gt;Foo&lt;any[]&gt; 实际上是不相关的类型。虽然重载的检查更宽松,但他们确实希望重载的返回类型和实现相关(实现返回或重载返回必须是另一个的子类型,ex)

为什么一些改变使它起作用:

关闭strictFunctionTypes 将使barCallback 站点为T 双变量,因此Foo 将是协变量 将barCallback 转换为方法,使T 的站点成为双变量,因此Foo 将是协变量的 删除 barCallback 将删除逆变用法,因此 Foo 将是协变的 删除baz 将删除T 的协变用法,使FooT 中逆变。

解决方法

您可以保持strictFunctionTypes 开启并为这个回调开辟一个例外,以保持它的双变量,通过使用双变量hack(解释here 用于更狭窄的用例,但同样的原则适用):


type BivariantCallback<C extends (... a: any[]) => any> =  bivarianceHack(...val: Parameters<C>): ReturnType<C> ["bivarianceHack"];


class Foo<T> 
    static manyFoo(): Foo<any[] |  [s: string]: any >;
    static manyFoo(): Foo<any[]> 
        return ['stub'] as any;
    

    barCallback!: BivariantCallback<(val: T) => void>;

    constructor() 
        // get synchronously from elsewhere
        (callback => 
            this.barCallback = callback;
        )((v: any) => );
    

    baz(callback: ((val: T) => void)): void 


Playground Link

【讨论】:

感谢您的详细解释。你又发了一篇很棒的 TS 帖子。 +1 来自我)。我在这里收集了一些与方差相关的问题/答案***.com/questions/66410115/…

以上是关于strictFunctionTypes 限制泛型类型的主要内容,如果未能解决你的问题,请参考以下文章

Java泛型

Nullable不是泛型类, 但方法可以是泛型的;

在Java中实例化一个泛型类[重复]

这个嵌套类构造函数片段可以应用于泛型类吗?

Java进阶之泛型

为啥 C# (4.0) 不允许泛型类类型中的协变和逆变?