简体   繁体   English

泛型类型在传递给另一个 function 时折叠为联合

[英]Generic type is collapsed to union when passed to another function

Hello fellow StackOverflowers!你好 StackOverflowers 同胞!

I have been trying for quite some time (with more complicated code) to achieve a generic type safe callback system that where one can register a callback to some event and this callback is added to an array of listeners for the specific event type.我已经尝试了很长一段时间(使用更复杂的代码)来实现一个通用类型安全的回调系统,在该系统中,人们可以注册某个事件的回调,并将该回调添加到特定事件类型的侦听器数组中。

It works fine when I don't use an array, but just one callback per event.当我不使用数组时它工作正常,但每个事件只有一个回调。 But typescript gets lost when I try to use an array instead to push the callback into.但是当我尝试使用数组代替将回调推送到时,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

Link to Typescript Playground 链接到 Typescript 游乐场

When pushing the callback into the array I get:将回调推入数组时,我得到:

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)

While before pushing, the compiler knows perfectly well that the type of C (the callback is):在推送之前,编译器非常清楚 C 的类型(回调是):

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

and the type of the callbacks array is:回调数组的类型是:

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

But when push ing they collapse to Callback<number> | Callback<string>但是当push他们崩溃到Callback<number> | Callback<string> Callback<number> | Callback<string> and Callback<number>[] | Callback<string>[] Callback<number> | Callback<string>Callback<number>[] | Callback<string>[] Callback<number>[] | Callback<string>[] respectively.分别Callback<number>[] | Callback<string>[] Have I run into a limitation of the typescript compiler or am I missing something obvious?我是否遇到了 typescript 编译器的限制,还是我遗漏了一些明显的东西? If it's a limitation, are there any workarounds?如果这是一个限制,是否有任何解决方法? Thanks!谢谢!

For ease of discussion I'm going to look at the following version of your code:为了便于讨论,我将查看您的代码的以下版本:

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? */ 
    }

}

which is pretty much the same, except that callback is of a properly generic type (in your version it gets widened to a union type ).这几乎是一样的,除了callback是一个适当的泛型类型(在你的版本中它被扩大到一个联合类型)。 And it still has the same problem in TypeScript 4.5 and below:在 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>[]

When you call callbacks.push() , the type of callbacks loses its genericness (genericity? genericality? whatever) and is seen only as a union.当您调用callbacks.push()时, callbacks的类型会失去其通用性(通用性?通用性?无论如何),并且仅被视为联合。 And since both callbacks and callback are either of a union type or constrained to a union type, the compiler forgets that they are correlated to each other.并且由于callbackscallback要么是联合类型,要么被限制为联合类型,编译器会忘记它们是相互关联的。 It worries about impossible situations, such as where callbacks is a Callback<number>[] while callback is a Callback<string> .它担心不可能的情况,例如callbacksCallback<number>[]callbackCallback<string>

This is, at least up until TypeScript 4.5, a design limitation (or missing feature) in TypeScript.这至少在 TypeScript 4.5 之前是 TypeScript 中的设计限制(或缺失功能)。 See microsoft/TypeScript#30581 for a detailed discussion.有关详细讨论,请参阅microsoft/TypeScript#30581


Luckily enough, a fix at microsoft/TypeScript#47109 should be released with TypeScript 4.6.幸运的是, microsoft/TypeScript#47109的修复程序应该与 TypeScript 4.6 一起发布。 Among other things, it maintains the genericosity (♂️) of callbacks.push() , and your problem goes away:除其他外,它保持了callbacks.push()的通用性 (♂️),您的问题就消失了:

// TS4.6+
callbacks.push(callback); // okay
// const callbacks: Callbacks[E]
// (method) Array<Callback<CallbackTypes[E]>>.push(
//   ...items: Callback<CallbackTypes[E]>[]): number

So if you can upgrade to typescript@next or wait until TS4.6 is released, then this problem will more or less resolve itself (as long as you redefine the callback parameter the way I do here).所以如果你可以升级到 typescript typescript@next或者等到 TS4.6 发布,那么这个问题或多或少会自行解决(只要你按照我这里的方式重新定义callback参数)。


Until then, all you can do is use a type assertion to tell the compiler what it can't figure out on its own.在那之前,您所能做的就是使用类型断言来告诉编译器它自己无法弄清楚什么。 For example:例如:

// TS4.5-
const callbacks = this.callbacks[event] as Callback<CallbackTypes[E]>[];
callbacks.push(callback); // okay

Now there's no error, because you've taken the job of maintaining type safety away from the compiler.现在没有错误了,因为您已经将维护类型安全的工作从编译器中移开。 You're giving your word that callbacks is a Callback<CallbackTypes[E]>[] , and the compiler believes you.您保证callbacksCallback<CallbackTypes[E]>[] ,并且编译器相信您。 As long as that turns out to be true, you won't have a problem.只要事实证明这是真的,你就不会有问题。 But if you lie to the compiler, accidentally or otherwise:但是如果你对编译器撒谎,无论是意外还是其他:

// TS4.5-
const callbacks = this.callbacks[MyEvent.One] 
  as Callback<CallbackTypes[E]>[]; // no compiler error 

you still won't have a compiler error but you can expect problems at runtime.您仍然不会遇到编译器错误,但您可能会在运行时遇到问题。


That means: if you use type assertions, take extra care to ensure that you're doing so responsibly.这意味着:如果您使用类型断言,请格外小心以确保您这样做是负责任的。 But hopefully you can make use of the fix at ms/TS#47109, which supports correlated unions without type assertions, and would catch errors like the one above:但希望您可以使用 ms/TS#47109 中的修复程序,该修复程序支持没有类型断言的相关联合,并且会捕获类似上述错误:

// TS4.6+
const callbacks = this.callbacks[MyEvent.One];
callbacks.push(callback); // error! 

TS4.5 Playground link to code TS4.5 Playground 代码链接

TS4.6 Playground link to code TS4.6 Playground 代码链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM