typescriptopentok

OpenTok event Typescript typing


Im trying to implement chat through OpenTok API using 'signal' events to publish/subscribe.

This is my event listener which works (the signal is received):

// Listen for signal CHAT_MESSAGE
sess.on('signal:CHAT_MESSAGE', event => {
 
  const message = event.data

  ...

}) 

Problem is that typescript doesn't recognize event.data as a valid property. The type comes from the Session class:

Session.signal: Event<'signal', Session> & {
    type?: string;
    data?: string;
    from: Connection;
};

I've tried picking the type from the Session class like

const message = event.data as Session['signal']

Typescript complains Property 'data' does not exist on type 'Event<string, any>'. I suspect it's because TS doesn't recognize the event type correctly...

I then tried converting to 'unknown' first:

const signal = (event as unknown) as Session['signal']
const msg = signal.data

Now TS complains: Property 'data' does not exist on type '(signal: { type?: string | undefined; data?: string | undefined; to?: Connection | undefined; }, callback: (error?: OTError | undefined) => void) => void'.ts(2339)

I'm not sure why it doesn't think that data is a prop when at the same time seemingly it says that it is...

How can I fix this problem, preferably without disabling TS type checking?


Solution

  • The problem is Session['signal'] is a function:

    signal(
        signal: { type?: string, data?: string, to?: Connection },
        callback: (error?: OTError) => void
    ): void;
    

    And the type you probably need is coming from the Session's class ancestor OTEventEmitter:

    export class Session extends OTEventEmitter<{
        ...
        signal: Event<'signal', Session> & {
          type?: string;
          data?: string;
          from: Connection;
        };
        ...
    

    OTEventEmitter.on method typings:

    class OTEventEmitter<EventMap> {
        on<EventName extends keyof EventMap>(
            eventName: EventName,
            callback: (event: EventMap[EventName]) => void,
            context?: object
        ): void;
    
        on(
            eventName: string,
            callback: (event: Event<string, any>) => void,
            context?: object
        ): void;
        ...
    

    As you may notice when Session extends OTEventEmitter there is no signal:CHAT_MESSAGE in the EventMap. Only signal.

    Thus if you want your signal:CHAT_MESSAGE to be treated as a signal event you should write it as:

    sess.on('signal:CHAT_MESSAGE' as 'signal', event => {
        const message = event.data // no error
        // `event` has type
        // Event<'signal', Session> & {
        //     type?: string;
        //     data?: string;
        //     from: Connection;
        // }
        ...
    

    This typecasting has no runtime artefacts. It's only purpose to ensure typescript that your signal:CHAT_MESSAGE event has the same structure as the signal's one. Though as always with as keyword now you're responsible for that to be true.