通用类型在传递给另一个函数时折叠为联合
Generic type is collapsed to union when passed to another function
Whosebugers 小伙伴们好!
我已经尝试了很长一段时间(使用更复杂的代码)来实现一个通用类型的安全回调系统,其中可以注册对某个事件的回调,并将该回调添加到特定事件的侦听器数组中事件类型。
当我不使用数组时它工作正常,但每个事件只有一个回调。但是当我尝试使用数组而不是将回调推送到其中时,typescript 丢失了。
enum MyEvent {
One,
Two
}
type Callback<T> = (arg: T) => void;
type CallbackTypes = {
[MyEvent.One]: number
[MyEvent.Two]: string
}
class CallbackContainer {
callbacksA: { [E in MyEvent]: Callback<CallbackTypes[E]> } = {
[MyEvent.One]: () => {},
[MyEvent.Two]: () => {}
};
callbacksB: { [E in MyEvent]: Callback<CallbackTypes[E]>[] } = {
[MyEvent.One]: [],
[MyEvent.Two]: []
};
constructor() {}
setListenerA<E extends MyEvent, C extends typeof this.callbacksA[E]>(this: CallbackContainer, event: E, callback: C) {
this.callbacksA[event] = callback; // =)
}
getListenerA<E extends MyEvent>(event: E) {
return this.callbacksA[event];
}
setListenerB<E extends MyEvent, C extends typeof this.callbacksB[E][number]>(this: CallbackContainer, event: E, callback: C) {
const callbacks = this.callbacksB[event]; // type of callbacks is still intact
callbacks.push(callback); // callbacks collapsed
}
getListenersB<E extends MyEvent>(event: E) {
return this.callbacksB[event];
}
}
const c = new CallbackContainer();
c.setListenerA(MyEvent.One, (a) => a + 1); // Types resolve fine
c.getListenerA(MyEvent.One); // Types resolve fine
c.setListenerB(MyEvent.One, (a) => a + 1); // Types resolve fine
c.getListenersB(MyEvent.One); // Types resolve fine
将回调推入数组时,我得到:
Argument of type 'Callback<number> | Callback<string>' is not assignable to parameter of type 'Callback<number> & Callback<string>'.
Type 'Callback<number>' is not assignable to type 'Callback<number> & Callback<string>'.
Type 'Callback<number>' is not assignable to type 'Callback<string>'.
Type 'string' is not assignable to type 'number'.ts(2345)
而在推送之前,编译器非常清楚 C 的类型(回调是):
C extends {
0: Callback<number>[];
1: Callback<string>[];
}[E][number]>
回调数组的类型是:
const callbacks = {
0: Callback<number>[];
1: Callback<string>[];
}[E]
但是当 push
ing 时,它们分别崩溃为 Callback<number> | Callback<string>
和 Callback<number>[] | Callback<string>[]
。我是否 运行 受到打字稿编译器的限制,或者我是否遗漏了一些明显的东西?如果这是一个限制,是否有任何解决方法?谢谢!
为了便于讨论,我将查看您的代码的以下版本:
type Callbacks = { [E in MyEvent]: Callback<CallbackTypes[E]>[] };
class CallbackContainer {
callbacks: Callbacks = {
[MyEvent.One]: [],
[MyEvent.Two]: []
};
constructor() { }
setListener<E extends MyEvent>(event: E, callback: Callback<CallbackTypes[E]>) {
/* how to implement? */
}
}
这几乎是一样的,除了 callback
是一个适当的通用类型(在你的版本中它被扩大到 union type)。在 TypeScript 4.5 及以下版本中仍然存在同样的问题:
// TS 4.5-
const callbacks = this.callbacks[event]
// const callbacks: Callbacks[E]
callbacks.push(callback); // error!
// const callbacks: Callback<number>[] | Callback<string>[]
当您调用 callbacks.push()
时,callbacks
的类型失去了它的通用性(通用性?通用性?随便什么)并且仅被视为联合。并且由于 callbacks
和 callback
都是联合类型或 constrained 联合类型,编译器忘记了它们与每个 correlated其他。它担心不可能的情况,例如 callbacks
是 Callback<number>[]
而 callback
是 Callback<string>
.
至少在 TypeScript 4.5 之前,这是 TypeScript 中的设计限制(或缺失的功能)。有关详细讨论,请参阅 microsoft/TypeScript#30581。
幸运的是,microsoft/TypeScript#47109 的修复应该随 TypeScript 4.6 一起发布。除其他外,它保持了 callbacks.push()
的通用性 (♂️),您的问题就消失了:
// TS4.6+
callbacks.push(callback); // okay
// const callbacks: Callbacks[E]
// (method) Array<Callback<CallbackTypes[E]>>.push(
// ...items: Callback<CallbackTypes[E]>[]): number
所以如果你能升级到typescript@next
或者等到TS4.6发布,那么这个问题或多或少会自行解决(只要你按照我的方式重新定义callback
参数在这里做)。
在那之前,您所能做的就是使用 type assertion 告诉编译器它自己无法解决的问题。例如:
// TS4.5-
const callbacks = this.callbacks[event] as Callback<CallbackTypes[E]>[];
callbacks.push(callback); // okay
现在没有错误了,因为您已经将维护类型安全的工作从编译器中拿走了。你说 callbacks
是 Callback<CallbackTypes[E]>[]
,编译器相信你。只要事实证明这是真的,你就不会有问题。但是如果你无意或以其他方式欺骗了编译器:
// TS4.5-
const callbacks = this.callbacks[MyEvent.One]
as Callback<CallbackTypes[E]>[]; // no compiler error
您仍然不会遇到编译器错误,但您可能会在运行时遇到问题。
这意味着:如果您使用类型断言,请格外小心以确保您这样做是负责任的。但希望您可以利用 ms/TS#47109 中的修复程序,它支持没有类型断言的相关联合,并且会捕获如上所示的错误:
// TS4.6+
const callbacks = this.callbacks[MyEvent.One];
callbacks.push(callback); // error!
Whosebugers 小伙伴们好!
我已经尝试了很长一段时间(使用更复杂的代码)来实现一个通用类型的安全回调系统,其中可以注册对某个事件的回调,并将该回调添加到特定事件的侦听器数组中事件类型。
当我不使用数组时它工作正常,但每个事件只有一个回调。但是当我尝试使用数组而不是将回调推送到其中时,typescript 丢失了。
enum MyEvent {
One,
Two
}
type Callback<T> = (arg: T) => void;
type CallbackTypes = {
[MyEvent.One]: number
[MyEvent.Two]: string
}
class CallbackContainer {
callbacksA: { [E in MyEvent]: Callback<CallbackTypes[E]> } = {
[MyEvent.One]: () => {},
[MyEvent.Two]: () => {}
};
callbacksB: { [E in MyEvent]: Callback<CallbackTypes[E]>[] } = {
[MyEvent.One]: [],
[MyEvent.Two]: []
};
constructor() {}
setListenerA<E extends MyEvent, C extends typeof this.callbacksA[E]>(this: CallbackContainer, event: E, callback: C) {
this.callbacksA[event] = callback; // =)
}
getListenerA<E extends MyEvent>(event: E) {
return this.callbacksA[event];
}
setListenerB<E extends MyEvent, C extends typeof this.callbacksB[E][number]>(this: CallbackContainer, event: E, callback: C) {
const callbacks = this.callbacksB[event]; // type of callbacks is still intact
callbacks.push(callback); // callbacks collapsed
}
getListenersB<E extends MyEvent>(event: E) {
return this.callbacksB[event];
}
}
const c = new CallbackContainer();
c.setListenerA(MyEvent.One, (a) => a + 1); // Types resolve fine
c.getListenerA(MyEvent.One); // Types resolve fine
c.setListenerB(MyEvent.One, (a) => a + 1); // Types resolve fine
c.getListenersB(MyEvent.One); // Types resolve fine
将回调推入数组时,我得到:
Argument of type 'Callback<number> | Callback<string>' is not assignable to parameter of type 'Callback<number> & Callback<string>'.
Type 'Callback<number>' is not assignable to type 'Callback<number> & Callback<string>'.
Type 'Callback<number>' is not assignable to type 'Callback<string>'.
Type 'string' is not assignable to type 'number'.ts(2345)
而在推送之前,编译器非常清楚 C 的类型(回调是):
C extends {
0: Callback<number>[];
1: Callback<string>[];
}[E][number]>
回调数组的类型是:
const callbacks = {
0: Callback<number>[];
1: Callback<string>[];
}[E]
但是当 push
ing 时,它们分别崩溃为 Callback<number> | Callback<string>
和 Callback<number>[] | Callback<string>[]
。我是否 运行 受到打字稿编译器的限制,或者我是否遗漏了一些明显的东西?如果这是一个限制,是否有任何解决方法?谢谢!
为了便于讨论,我将查看您的代码的以下版本:
type Callbacks = { [E in MyEvent]: Callback<CallbackTypes[E]>[] };
class CallbackContainer {
callbacks: Callbacks = {
[MyEvent.One]: [],
[MyEvent.Two]: []
};
constructor() { }
setListener<E extends MyEvent>(event: E, callback: Callback<CallbackTypes[E]>) {
/* how to implement? */
}
}
这几乎是一样的,除了 callback
是一个适当的通用类型(在你的版本中它被扩大到 union type)。在 TypeScript 4.5 及以下版本中仍然存在同样的问题:
// TS 4.5-
const callbacks = this.callbacks[event]
// const callbacks: Callbacks[E]
callbacks.push(callback); // error!
// const callbacks: Callback<number>[] | Callback<string>[]
当您调用 callbacks.push()
时,callbacks
的类型失去了它的通用性(通用性?通用性?随便什么)并且仅被视为联合。并且由于 callbacks
和 callback
都是联合类型或 constrained 联合类型,编译器忘记了它们与每个 correlated其他。它担心不可能的情况,例如 callbacks
是 Callback<number>[]
而 callback
是 Callback<string>
.
至少在 TypeScript 4.5 之前,这是 TypeScript 中的设计限制(或缺失的功能)。有关详细讨论,请参阅 microsoft/TypeScript#30581。
幸运的是,microsoft/TypeScript#47109 的修复应该随 TypeScript 4.6 一起发布。除其他外,它保持了 callbacks.push()
的通用性 (♂️),您的问题就消失了:
// TS4.6+
callbacks.push(callback); // okay
// const callbacks: Callbacks[E]
// (method) Array<Callback<CallbackTypes[E]>>.push(
// ...items: Callback<CallbackTypes[E]>[]): number
所以如果你能升级到typescript@next
或者等到TS4.6发布,那么这个问题或多或少会自行解决(只要你按照我的方式重新定义callback
参数在这里做)。
在那之前,您所能做的就是使用 type assertion 告诉编译器它自己无法解决的问题。例如:
// TS4.5-
const callbacks = this.callbacks[event] as Callback<CallbackTypes[E]>[];
callbacks.push(callback); // okay
现在没有错误了,因为您已经将维护类型安全的工作从编译器中拿走了。你说 callbacks
是 Callback<CallbackTypes[E]>[]
,编译器相信你。只要事实证明这是真的,你就不会有问题。但是如果你无意或以其他方式欺骗了编译器:
// TS4.5-
const callbacks = this.callbacks[MyEvent.One]
as Callback<CallbackTypes[E]>[]; // no compiler error
您仍然不会遇到编译器错误,但您可能会在运行时遇到问题。
这意味着:如果您使用类型断言,请格外小心以确保您这样做是负责任的。但希望您可以利用 ms/TS#47109 中的修复程序,它支持没有类型断言的相关联合,并且会捕获如上所示的错误:
// TS4.6+
const callbacks = this.callbacks[MyEvent.One];
callbacks.push(callback); // error!