StrictFunctionTypes限制泛型类型 [英] strictFunctionTypes restricts generic type
问题描述
该问题似乎与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
类型时,才会出现问题。
如果barCallback
或baz
不使用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>
,以及两个相关类型Animal
和Dog extends Animal
。Foo<Animal>
和Foo<Dog>
之间有四种可能的关系:
- 协方差-
Foo<Animal>
和Foo<Dog>
的继承点箭头与Animal
和Dog
的方向相同,因此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; // ✅
- 逆方差-
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; // 🚫
- 不变性-虽然
Dog
和Animal
是相关的Foo<Animal>
和Foo<Dog>
之间没有任何关系,因此两者都不能分配给对方。
type InVariant<T> = (p: T) => T
declare var inAnimal: InVariant<Animal>
declare var inDog: InVariant<Dog>
inDog = inAnimal; // 🚫
inAnimal = inDog; // 🚫
- 互变-如果
Dog
和Animal
相关,则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中,类型参数的位置决定方差,例如:
- Co-varaint-
T
在中用作字段或函数的返回类型 - Contra-varaint-
T
作为strictFunctionTypes
下的函数签名的参数
- 不变-
T
同时用于协变和逆变位置 - 双变量-
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
在第一个示例中,我们传递给aliasDogCbAsAnimalCb
的回调预计会收到Animal
,因此它只使用Animal
成员。实现withDogCb
将创建一个Dog
并将其传递给回调,但这很好。回调将只使用它预期的基类属性即可正常工作。
在第二个示例中,我们传递给aliasAnimalCbAsDogCb
的回调预期接收Dog
,因此它使用Dog
成员。但实现withAnimalCb
将向回调传递一个动物的实例。这可能导致运行时错误,因为回调最终使用了不存在的成员。
因此,如果只将FunctionWithCallback<Dog>
赋给FunctionWithCallback<Animal>
是安全的,我们得出的结论是,T
的这种用法决定了协方差。
结论
因此T
在Foo
中同时用于协变和逆变位置,这意味着Foo
在T
中是不变的。这意味着,就类型系统而言,Foo<any[] | { [s: string]: any }>
和Foo<any[]>
实际上是不相关的类型。虽然重载在他们的检查中比较宽松,但它们确实希望重载的返回类型和实现相关(实现返回或重载返回必须是另一个的子类型ex)
为什么某些更改会使其正常工作:
- 关闭
strictFunctionTypes
将使T
的barCallback
站点为双变量,因此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 {}
}
这篇关于StrictFunctionTypes限制泛型类型的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!