I need to cover with types this code:
interface MessageData {
offscreen: {
start: { streamId: string }
setVolume: { volume: string }
stop: void
}
service_worker: {
test: { testData: number }
}
}
function addEventListener(msg) {
if (msg.target !== 'offscreen') return
if (msg.action === 'start') start(msg.data.streamId)
else if (msg.action === 'setVolume') setVolume(msg.data.volume)
else if (msg.action === 'stop') stop()
}
And this is what I came to
function addEventListener<T extends keyof MessageData, A extends keyof MessageData[T], D extends MessageData[T][A]>(msg: { target: T, action: A, data: D }) {
if (msg.target !== 'offscreen') return
if (msg.action === 'start') start(msg.data.streamId)
else if (msg.action === 'setVolume') setVolume(msg.data.volume)
else if (msg.action === 'stop') stop()
}
For example, this code works, and the sendMessage method correctly resolves types when using the function.
function sendMessage<T extends keyof MessageData, A extends keyof MessageData[T], D extends MessageData[T][A]>(target: T, action: A, data?: D) {
return chrome.runtime.sendMessage({ target, action, data })
}
And I don't understand why this doesn't happen in addEventListener.
In order to get narrowing when you check .action (so that TypeScript knows that msg.data.streamId exists when msg.action === 'start', for example), msg needs to be typed as a concrete discriminated union, like this:
type Message = {
target: 'offscreen'
action: 'start'
data: { streamId: string }
} | {
target: 'offscreen'
action: 'setVolume'
data: { volume: string }
} | {
target: 'offscreen'
action: 'stop'
data: void
} | {
target: 'service_worker'
action: 'test'
data: { testData: number }
}
To avoid repeating yourself you could derive that type from MessageData using nested mapped types, like so:
type Message = {
[T in keyof MessageData]: {
[A in keyof MessageData[T]]: {
target: T
action: A
data: MessageData[T][A]
}
}[keyof MessageData[T]]
}[keyof MessageData]
The indexed access operation at the end of each of those mapped types ([keyof MessageData[T]] and [keyof MessageData]) pulls out a union of the property value types, so at the end Message is a flat union type as shown above.