Greg
Greg

Reputation: 803

Typescript for typical observer pattern

I'm trying to write typescript for a structure like observer pattern and I'm failing to achieve this. I think I'm almost done but for some reason I don't understand why the ts is complaining.

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

When I change the Listeners type to

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

then register function works ok, but the trigger doesn't shows any type on the listener object in forEach as it's obvious. So the option there is casting what I don't like. What I am doing wrong here?

Upvotes: 1

Views: 518

Answers (1)

ABabin
ABabin

Reputation: 2947

If you take a look at the index type examples in the docs, it seems that generically indexing only works when the type of the object being indexed is itself part of the generic context of the function, as opposed to a specific object outside of it.

Inside the registerListener function, the type of listener in this.listeners[name].push(listener) is expected to be an intersection of OnChangeListener and OnCreateListener because it doesn't know what whether name will be "onChange" or "onCreate" and will thus only accept function signatures that are applicable to both. Typescript doesn't do the generic index magic unless the object type is generic as well.

Since registerListener does properly discriminate the listener signature that should be passed for the given key, I think the best solution is just to tell Typescript that the listener array you're accessing matches the listener passed.


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

Upvotes: 1

Related Questions