Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I make Electron channels more type safe?

Here is how I handle communication over Electron channels:

preload.ts

contextBridge.exposeInMainWorld("myIpcRenderer", {
  invoke: (channel: Channel, ...args: any[]) =>
    callIpcRenderer("invoke", channel, ...args),
  send: (channel: Channel, ...args: any[]) =>
    callIpcRenderer("send", channel, ...args),
  on: (channel: Channel, ...args: any[]) =>
    callIpcRenderer("on", channel, ...args),
});

types.d.ts

interface MyIpcRenderer {
  invoke(channel: Channel.ReadFiles, ...args: any[]): Promise<ReadFileResult[]>;
}

The MyIpcRenderer type enforce correct use in renderer.ts:

const files = await window.myIpcRenderer.invoke(Channel.ReadFiles, [
  svgPath,
]);

But it does not enforce correct use in main.ts. ipcMain.handle refers to an electron method that can accept channels with any name and retturn any kind of promise.

ipcMain.handle(
  "anychannelnamegoes",
  async (_event, paths: string[]): Promise<any> => {
    ...
  }
);

How can I rewrite my code to enforce main usage in main.ts as well?

like image 230
user1283776 Avatar asked Nov 07 '22 07:11

user1283776


1 Answers

I see two ways to do this.

First possibility is to override Electron library types declaration in your typings.d.ts file:

declare module 'electron' {
  export interface IpcMain extends NodeJS.EventEmitter {
    handle(
      channel: Channel,
      listener: (
        event: IpcMainInvokeEvent,
        ...args: any[]
      ) => Promise<void> | any
    ): void;
  }
}

Considering that Channel is a string literal (type Channel = 'channel1' | 'channel2'), you would get suggestions for 'channel1' and 'channel2', but the initial signature channel: string would still be allowed and any string would be accepted.

The second possibility I see is to wrap it, which would give better type safety:

const myHandler = (
  channel: Channel,
  listener: (
    event: IpcMainInvokeEvent,
    ...args: any[]
  ) => Promise<void> | any
) => ipcMain.handle(channel, listener);

// only 'channel1' and 'channel2' would be accepted
myHandler('channel1', (event) => {});

I you would like to type the arguments currently declared with the spread operator ...args: any[] you would need to declare the parameters explicitly in both cases:

...
listener: (
  event: IpcMainInvokeEvent,
  arg1: string[],
  arg2: boolean,
  arg3: number
) => Promise<void> | any
...
like image 150
Guillaume Avatar answered Nov 14 '22 02:11

Guillaume