典型观察者模式的打字稿

Typescript for typical observer pattern

我正在尝试为类似观察者模式的结构编写打字稿,但未能实现。我想我快完成了,但出于某种原因我不明白为什么 ts 会抱怨。

type OnChangeListener = (object: object, from: string, to: string) => void
type OnCreateListener = (object: object) => void

type ListenerTypes = {
    onChange: OnChangeListener,
    onCreate: OnCreateListener
}

type Listener<T extends keyof ListenerTypes> = ListenerTypes[T]

type Listeners = {
    onChange: OnChangeListener[],
    onCreate: OnCreateListener[]
}

class SomeClass {
    private listeners: Listeners = {
        onChange: [],
        onCreate: []
    }

    public create(object: object) {
        this.listeners.onCreate.forEach(listener => listener(object))
    }

    public change(object: object, from: string, to: string) {
        this.listeners.onChange.forEach(listener => listener(object, from, to))
    }

    public registerListener<T extends keyof ListenerTypes>(name: T, listener: Listener<T>): void
    {
        this.listeners[name].push(listener) // <-- here is a problem
        /*
Argument of type 'Listener<T>' is not assignable to parameter of type 'OnCreateListener'.
  Type 'OnChangeListener | OnCreateListener' is not assignable to type 'OnCreateListener'.
    Type 'OnChangeListener' is not assignable to type 'OnCreateListener'
        */
    }
}

const someObject = new SomeClass()

someObject.registerListener("onChange", (o, from, to) => {}) // <-- this is ok
someObject.registerListener("onCreate", (o) => {}) // <-- this is ok
someObject.registerListener("onCreate",  (o, from, to) => {}) // <-- this error is ok, as the callback should be different

当我将监听器类型更改为

type Listeners = {
    onChange: any[],
    onCreate: any[]
}

然后注册函数工作正常,但触发器不会在 forEach 中的侦听器对象上显示任何类型,因为它很明显。所以那里的选择是铸造我不喜欢的东西。我在这里做错了什么?

如果您查看文档中的索引类型 examples,似乎一般索引仅在被索引对象的类型本身是函数的通用上下文的一部分时才有效,因为反对它之外的特定对象。

registerListener函数中,this.listeners[name].push(listener)listener的类型应该是OnChangeListenerOnCreateListener的交集,因为它不是'不知道 name"onChange" 还是 "onCreate",因此只会接受适用于两者的函数签名。除非对象类型也是通用的,否则 Typescript 不会执行通用索引魔术。

因为 registerListener 确实 正确区分应该为给定键传递的侦听器签名,我认为最好的解决方案是告诉 Typescript 侦听器数组您正在访问的与通过的侦听器相匹配。


public registerListener<T extends keyof ListenerTypes>(name: T, listener: Listener<T>): void {
    (this.listeners[name] as Listener<T>[]).push(listener);  
}