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
类型时才会出现问题。
如果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
会以这种方式影响类类型。
【问题讨论】:
为什么barCallback
不能用方法签名代替? typescriptlang.org/play?#code/…
抱歉,在写这篇文章的时候丢失了一个重要的部分。是的,方法签名存在问题,因为它最初是 barCallback!
并同步设置,只是不在构造函数范围内。
【参考方案1】:
这是其核心的差异问题。所以首先是差异入门:
关于方差
给定一个泛型类型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>
的继承箭头指向 相反方向,就像 Animal
和 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; // ?
-
Bivariance - 如果
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
的使用如何影响方差?在打字稿中,类型参数的位置决定了方差,一些例子:
-
Co-variint -
T
用作字段或函数的返回类型
Contra-variint - T
用作strictFunctionTypes
下的函数 签名的参数
不变 - T
用于协变和逆变位置
Bi-variant - T
用作strictFunctionTypes
下的方法 定义的参数,或者如果strictFunctionTypes
关闭,则用作方法或函数的参数类型。李>
strictFunctionTypes
中方法和函数参数行为不同的原因解释here:
更严格的检查适用于所有函数类型,但源自方法或构造函数声明的函数类型除外。专门排除方法以确保泛型类和接口(例如 Array)继续主要以协变方式相关。严格检查方法的影响将是一个更大的突破性变化,因为大量泛型类型将变得不变(即便如此,我们可能会继续探索这种更严格的模式)。
回到问题
让我们看看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
的这种用法决定了协方差。
结论
所以我们在Foo
的协变和逆变位置都使用了T
,这意味着Foo
在T
中是不变的。这意味着就类型系统而言,Foo<any[] | [s: string]: any >
和Foo<any[]>
实际上是不相关的类型。虽然重载的检查更宽松,但他们确实希望重载的返回类型和实现相关(实现返回或重载返回必须是另一个的子类型,ex)
为什么一些改变使它起作用:
关闭strictFunctionTypes
将使barCallback
站点为T
双变量,因此Foo
将是协变量
将barCallback
转换为方法,使T
的站点成为双变量,因此Foo
将是协变量的
删除 barCallback
将删除逆变用法,因此 Foo
将是协变的
删除baz
将删除T
的协变用法,使Foo
在T
中逆变。
解决方法
您可以保持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 限制泛型类型的主要内容,如果未能解决你的问题,请参考以下文章