type Event<S extends string> = {
name: S
}
const EventHandler = <S extends string, T extends Event<S>[]>(arr: [...T]) => {
let lastEvent: T[number] = arr[0]
return {
set: (event: T[number]) => lastEvent = event,
emit: (event: typeof lastEvent['name']) => { },
lastEvent
}
}
const emitter = EventHandler([{ name: 'A' }, { name: 'B' }])
if(emitter.lastEvent.name === 'A') {
// `emitter.lastEvent` is now inferred to `{ name: A }`
emitter.emit('B') // compiles, but I want it to only allow the string `A`
}
In this contrived example, I create some object EventHandler
that has some configuration lastEvent
. Now, I want the parameter of emit
to be narrowed down depending on the configuration.
However, even though the type of lastEvent
gets narrowed down in the conditional, I'm unable to narrow down the input of emit
as a result of that.
Is this possible? Preferably without using user defined type guards...
[EDIT]:
I thought I could make a compromise and add the event object as input to emit
:
emit: (event: T[number], name: typeof event['name']) => { }
and then do:
emitter.emit({ name: 'A' }, 'B') // why does this compile ?!
but that also doesn't get inferred! This seems strange - why can't typescript handle this?
CodePudding user response:
To solve this, we would ideally build a discriminated union so that the type of the emit
function can be narrowed by checking the type of lastEvent
. This would require us to check the nested value of emitter.lastEvent.name
. But that's a problem since TypeScript currently does not allow narrowing discrimanted unions based on deeply nested properties (see #18758).
What we can do for now is to avoid the nesting and have the string value of name
on the top-level of the object.
We can use the union of T[number]
with a mapped type to build a discriminated union where each union constituent K
is the discriminant.
const EventHandler = <T extends string[]>(arr: [...T]) => {
let lastEvent: T[number] = arr[0]
return {
set: (event: T[number]) => lastEvent = event,
emit: (event: typeof lastEvent) => { },
lastEvent
} as {
set: (event: T[number]) => void
} & {
[K in T[number]]: {
emit: (event: K) => void
lastEvent: K
}
}[T[number]]
}
This lets us properly narrow the type of emit
by checking the value of lastEvent
.
const emitter = EventHandler(['A', 'B'])
if(emitter.lastEvent === 'A') {
emitter.emit('A')
emitter.emit('B') // Error: Argument of type '"B"' is not assignable
// to parameter of type '"A"'
}
if(emitter.lastEvent === 'B') {
emitter.emit('A') // Error: Argument of type '"A"' is not assignable
// to parameter of type '"B"'
emitter.emit('B')
}
emitter.set('A')
emitter.set('B')