传递给另一个函数时,泛型类型会折叠为联合

qui*_*tic 5 generics typescript

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
Run Code Online (Sandbox Code Playgroud)

链接到 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)
Run Code Online (Sandbox Code Playgroud)

在推送之前,编译器非常清楚 C 的类型(回调是):

C extends {
    0: Callback<number>[];
    1: Callback<string>[];
}[E][number]>
Run Code Online (Sandbox Code Playgroud)

回调数组的类型是:

const callbacks = {
    0: Callback<number>[];
    1: Callback<string>[];
}[E]
Run Code Online (Sandbox Code Playgroud)

但当pushing 时,它们分别折叠为Callback<number> | Callback<string> Callback<number>[] | Callback<string>[]。我是否遇到了打字稿编译器的限制,或者我是否遗漏了一些明显的东西?如果这是一个限制,有什么解决方法吗?谢谢!

jca*_*alz 4

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

\n
type Callbacks = { [E in MyEvent]: Callback<CallbackTypes[E]>[] };\n\nclass CallbackContainer {\n\n    callbacks: Callbacks = {\n        [MyEvent.One]: [],\n        [MyEvent.Two]: []\n    };\n\n    constructor() { }\n\n    setListener<E extends MyEvent>(event: E, callback: Callback<CallbackTypes[E]>) { \n      /* how to implement? */ \n    }\n\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这几乎是相同的,除了它callback是一个适当的通用类型(在您的版本中它被扩展为联合类型)。在 TypeScript 4.5 及以下版本中仍然存在同样的问题:

\n
// TS 4.5-\nconst callbacks = this.callbacks[event]\n// const callbacks: Callbacks[E]\ncallbacks.push(callback); // error!\n// const callbacks: Callback<number>[] | Callback<string>[]\n
Run Code Online (Sandbox Code Playgroud)\n

当您调用 时callbacks.push(), 的类型会callbacks失去其通用性(通用性?通用性?无论如何)并且仅被视为联合。由于 和callbacks都是callback联合类型或约束为联合类型,因此编译器会忘记它们彼此相关。它担心不可能的情况,例如 where callbacksis a Callback<number>[]whilecallback是 a Callback<string>

\n

至少在 TypeScript 4.5 之前,这是 TypeScript 中的一个设计限制(或缺失的功能)。有关详细讨论,请参阅microsoft/TypeScript#30581 。

\n
\n

幸运的是, microsoft/TypeScript#47109上的修复应该随 TypeScript 4.6 一起发布。除此之外,它保持了通用性(\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f)callbacks.push(),你的问题就消失了:

\n
// TS4.6+\ncallbacks.push(callback); // okay\n// const callbacks: Callbacks[E]\n// (method) Array<Callback<CallbackTypes[E]>>.push(\n//   ...items: Callback<CallbackTypes[E]>[]): number\n
Run Code Online (Sandbox Code Playgroud)\n

所以如果你能升级到typescript@next或者等到TS4.6发布,那么这个问题或多或少会自行解决(只要你重新定义callback我这里的方式重新定义参数)。

\n
\n

在那之前,您所能做的就是使用类型断言来告诉编译器它自己无法弄清楚什么。例如:

\n
// TS4.5-\nconst callbacks = this.callbacks[event] as Callback<CallbackTypes[E]>[];\ncallbacks.push(callback); // okay\n
Run Code Online (Sandbox Code Playgroud)\n

现在没有错误了,因为您已经将维护类型安全的工作从编译器手中夺走了。你承诺的callbacksCallback<CallbackTypes[E]>[],编译器相信你。只要事实证明这是真的,你就不会有问题。但如果你无意或以其他方式对编译器撒了谎:

\n
// TS4.5-\nconst callbacks = this.callbacks[MyEvent.One] \n  as Callback<CallbackTypes[E]>[]; // no compiler error \n
Run Code Online (Sandbox Code Playgroud)\n

您仍然不会遇到编译器错误,但您可能会在运行时出现问题。

\n
\n

这意味着:如果您使用类型断言,请格外小心以确保您负责任地这样做。但希望您可以利用 ms/TS#47109 中的修复程序,该修复程序支持没有类型断言的相关联合,并且可以捕获类似上面的错误:

\n
// TS4.6+\nconst callbacks = this.callbacks[MyEvent.One];\ncallbacks.push(callback); // error! \n
Run Code Online (Sandbox Code Playgroud)\n

TS4.5 Playground 代码链接

\n

TS4.6 Playground 代码链接

\n