StrictFunctionTypes限制泛型类型 [英] strictFunctionTypes restricts generic type

查看:12
本文介绍了StrictFunctionTypes限制泛型类型的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

该问题似乎与strictFunctionTypes如何影响泛型类类型有关。

这里有一个类,它紧密地再现了发生的事情,由于要求不能进一步简化,any用于指定不加额外限制的部分(aplayground):

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 {}
}

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

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

仅当T用作barCallback函数类型中的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会以这种方式影响类类型。

推荐答案

这本质上是一个差异问题。所以,首先是一个差异入门:

关于差异

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

  1. 协方差-Foo<Animal>Foo<Dog>的继承点箭头与AnimalDog的方向相同,因此Foo<Dog>Foo<Animal>的子类型,这也意味着Foo<Dog>可以赋值给Foo<Animal>
type CoVariant<T> = () => T
declare var coAnimal: CoVariant<Animal>
declare var coDog: CoVariant<Dog>
coDog = coAnimal; // 🚫
coAnimal = coDog; // ✅
  1. 逆方差-Foo<Animal>Foo<Dog>Foo<Animal>Foo<Dog>方向相反的继承点箭头,因此Foo<Animal>实际上是Foo<Dog>的子类型,这也意味着Foo<Animal>可以赋值给Foo<Dog>
type ContraVariant<T> = (p: T) => void
declare var contraAnimal: ContraVariant<Animal>
declare var contraDog: ContraVariant<Dog>
contraDog = contraAnimal; // ✅
contraAnimal = contraDog; // 🚫
  1. 不变性-虽然DogAnimal是相关的Foo<Animal>Foo<Dog>之间没有任何关系,因此两者都不能分配给对方。
type InVariant<T> = (p: T) => T
declare var inAnimal: InVariant<Animal>
declare var inDog: InVariant<Dog>
inDog = inAnimal; // 🚫
inAnimal = inDog; // 🚫
  1. 互变-如果DogAnimal相关,则Foo<Animal>都是Foo<Dog>的子类型,Foo<Animal>Foo<Dog>的子类型,表示任何一种类型都可以分配给另一种类型。在更严格的类型系统中,这将是一种病理情况,其中可能实际上不使用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的使用如何影响差异?在TypeScrip中,类型参数的位置决定方差,例如:

  1. Co-varaint-T在中用作字段或函数的返回类型
  2. Contra-varaint-T作为strictFunctionTypes
  3. 下的函数签名的参数
  4. 不变-T同时用于协变和逆变位置
  5. 双变量-T作为strictFunctionTypes方法定义的参数,如果strictFunctionTypes为OFF,则作为方法或函数的参数类型。

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

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

返回问题

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

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

  • baz(callback: ((val: T) => 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<Dog>赋给FunctionWithCallback<Animal>是安全的,我们得出的结论是,T的这种用法决定了协方差。

结论

因此TFoo中同时用于协变和逆变位置,这意味着FooT中是不变的。这意味着,就类型系统而言,Foo<any[] | { [s: string]: any }>Foo<any[]>实际上是不相关的类型。虽然重载在他们的检查中比较宽松,但它们确实希望重载的返回类型和实现相关(实现返回或重载返回必须是另一个的子类型ex)

为什么某些更改会使其正常工作:

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

解决方法

您可以保留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

这篇关于StrictFunctionTypes限制泛型类型的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆