Reputation: 803
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
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