【问题标题】:Generic type is collapsed to union when passed to another function泛型类型在传递给另一个函数时折叠为联合
【发布时间】:2021-12-30 00:15:39
【问题描述】:

大家好 StackOverflowers!

我已经尝试了很长一段时间(使用更复杂的代码)来实现一个通用类型安全的回调系统,在该系统中,人们可以注册某个事件的回调,并且这个回调被添加到特定事件类型的侦听器数组中.

当我不使用数组时它工作正常,但每个事件只有一个回调。但是当我尝试使用数组而不是将回调推送到其中时,打字稿会丢失。

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

Link to Typescript Playground

将回调推入数组时,我得到:

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)

在 push 之前,编译器完全知道 C 的类型(回调是):

C extends {
    0: Callback<number>[];
    1: Callback<string>[];
}[E][number]>

回调数组的类型是:

const callbacks = {
    0: Callback<number>[];
    1: Callback<string>[];
}[E]

但是当pushing 时,它们分别崩溃为Callback&lt;number&gt; | Callback&lt;string&gt; Callback&lt;number&gt;[] | Callback&lt;string&gt;[]。我是否遇到了打字稿编译器的限制,或者我是否遗漏了一些明显的东西?如果这是一个限制,是否有任何解决方法?谢谢!

【问题讨论】:

  • 调用签名setListener&lt;E extends MyEvent, C extends typeof this.callbacksB[E][number]&gt;(this: CallbackContainer, event: E, callback: C) 比这里看起来需要的要复杂得多; setListener&lt;E extends MyEvent&gt;(event: E, callback: Callback&lt;CallbackTypes[E]&gt;) 对你有用吗?在我写的答案中,我会假设是这样。
  • setListener&lt;E extends MyEvent&gt;(event: E, callback: Callback&lt;CallbackTypes[E]&gt;) 也可以。我只是想让编译器工作,所以typeof this 部分是一种反复试验。

标签: typescript generics


【解决方案1】:

为了便于讨论,我将查看您的代码的以下版本:

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 的类型失去了它的泛型性(泛型性?泛型性?随便什么),并且只被视为一个联合。由于callbackscallback 都属于联合类型或constrained 属于联合类型,因此编译器会忘记它们彼此之间存在相关。它担心不可能的情况,例如callbacksCallback&lt;number&gt;[]callbackCallback&lt;string&gt;

至少在 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

现在没有错误了,因为您已将维护类型安全的工作从编译器中移开。你保证callbacksCallback&lt;CallbackTypes[E]&gt;[],编译器相信你。只要事实证明这是真的,你就不会有问题。但如果你对编译器撒谎,不管是意外还是其他:

// 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! 

TS4.5 Playground link to code

TS4.6 Playground link to code

【讨论】:

  • 您说“...除了回调是适当的泛型类型(在您的版本中,它被扩展为联合类型)”。使用typeof this. 会导致那里转换为联合吗?
  • 排序;编译器看不到Callbacks[E][number]Callback&lt;CallbackTypes[E]&gt; 相同,即使E 不是完整的MyEvent 联合时必须如此。为了使编译器更容易,我们希望 Parameters&lt;typeof callbacks.push&gt;[0]typeof callback 完全相同。
  • 在您的 4.6 操场中,当我将 const callbacks = 更改为 const callbacks: Array&lt;Callback&lt;CallbackTypes[E]&gt;&gt; = 时,编译器会显示 'Callbacks[E]' is not assignable to type 'Callback&lt;CallbackTypes[E]&gt;[]',而这正是回调在检查 .push 时所具有的确切类型。为什么会发生这种情况?
  • 我真的不知道 quickinfo 如何在 TS4.6 中使用这些分布式对象类型的细节;这是一个很好的问题,也许应该单独提出,或者在ms/TS#47109 或其他地方发表评论。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-03-07
  • 2021-05-05
  • 1970-01-01
  • 2021-07-24
  • 1970-01-01
  • 1970-01-01
  • 2011-02-20
相关资源
最近更新 更多