'contextBridge.exposeInMainWorld and IPC with Typescript in Electron app: Cannot read property 'send' of undefined

I defined contextBridge ( https://www.electronjs.org/docs/all#contextbridge ) in preload.js as follows:

const {
  contextBridge,
  ipcRenderer
} = require("electron")

contextBridge.exposeInMainWorld(
  "api", {
      send: (channel, data) => {
          ipcRenderer.invoke(channel, data).catch(e => console.log(e))
      },
      receive: (channel, func) => {
        console.log("preload-receive called. args: ");
        ipcRenderer.on(channel, (event, ...args) => func(...args));
      },
      // https://www.electronjs.org/docs/all#ipcrenderersendtowebcontentsid-channel-args
      electronIpcSendTo: (window_id: string, channel: string, ...arg: any) => {
        ipcRenderer.sendTo(window_id, channel, arg);
      },
      // https://github.com/frederiksen/angular-electron-boilerplate/blob/master/src/preload
/preload.ts
      electronIpcSend: (channel: string, ...arg: any) => {
        ipcRenderer.send(channel, arg);
      },
      electronIpcSendSync: (channel: string, ...arg: any) => {
        return ipcRenderer.sendSync(channel, arg);
      },
      electronIpcOn: (channel: string, listener: (event: any, ...arg: any) => void) => {
        ipcRenderer.on(channel, listener);
      },
      electronIpcOnce: (channel: string, listener: (event: any, ...arg: any) => void) => {
        ipcRenderer.once(channel, listener);
      },
      electronIpcRemoveListener:  (channel: string, listener: (event: any, ...arg: any) => 
void) => {
        ipcRenderer.removeListener(channel, listener);
      },
      electronIpcRemoveAllListeners: (channel: string) => {
        ipcRenderer.removeAllListeners(channel);
      }
  }
)

I defined a global.ts :

export {}
declare global {
  interface Window {
    "api": {
      send: (channel: string, ...arg: any) => void;
      receive: (channel: string, func: (event: any, ...arg: any) => void) => void;
      // https://github.com/frederiksen/angular-electron-boilerplate/blob/master/src/preload
/preload.ts
      // https://www.electronjs.org/docs/all#ipcrenderersendtowebcontentsid-channel-args
      electronIpcSendTo: (window_id: string, channel: string, ...arg: any) => void;
      electronIpcSend: (channel: string, ...arg: any) => void;
      electronIpcOn: (channel: string, listener: (event: any, ...arg: any) => void) => void;
      electronIpcSendSync: (channel: string, ...arg: any) => void;
      electronIpcOnce: (channel: string, listener: (event: any, ...arg: any) => void) => 
void;
      electronIpcRemoveListener:  (channel: string, listener: (event: any, ...arg: any) =>
 void) => void;
      electronIpcRemoveAllListeners: (channel: string) => void;
    }
  }
}

and in the renderer process App.tsx I call window.api.send :

window.api.send('open-type-A-window', ''); 

The typescript compilation looks fine:

yarn run dev
yarn run v1.22.5 
$ yarn run tsc && rimraf dist && cross-env NODE_ENV=development webpack --watch --progress 
--color
$ tsc
95% emitting emit(node:18180) [DEP_WEBPACK_COMPILATION_ASSETS] DeprecationWarning:      
Compilation.assets will be frozen in future, all modifications are deprecated.

BREAKING CHANGE: No more changes should happen to Compilation.assets after sealing the 
Compilation.
    Do changes to assets earlier, e. g. in Compilation.hooks.processAssets.
    Make sure to select an appropriate stage from Compilation.PROCESS_ASSETS_STAGE_*.
(Use `node --trace-deprecation ...` to show where the warning was created)
asset main.bundle.js 32.6 KiB [emitted] (name: main) 1 related asset
asset package.json 632 bytes [emitted] [from: package.json] [copied]
cacheable modules 26.2 KiB
  modules by path ./node_modules/electron-squirrel-startup/ 18.7 KiB
    modules by path ./node_modules/electron-squirrel-startup/node_modules/debug/src/*.js 15 
KiB 4 modules
    ./node_modules/electron-squirrel-startup/index.js 1 KiB [built] [code generated]
    ./node_modules/electron-squirrel-startup/node_modules/ms/index.js 2.7 KiB [built] [code 
generated]
  ./src/main/main.ts 6.82 KiB [built] [code generated]
  ./node_modules/file-url/index.js 684 bytes [built] [code generated]
external "path" 42 bytes [built] [code generated]
external "url" 42 bytes [built] [code generated]
external "electron" 42 bytes [built] [code generated]
external "child_process" 42 bytes [built] [code generated]
external "tty" 42 bytes [built] [code generated]
external "util" 42 bytes [built] [code generated]
external "fs" 42 bytes [built] [code generated]
external "net" 42 bytes [built] [code generated]
webpack 5.21.2 compiled successfully in 4313 ms

asset renderer.bundle.js 1000 KiB [emitted] (name: main) 1 related asset
asset index.html 196 bytes [emitted]
runtime modules 937 bytes 4 modules
modules by path ./node_modules/ 990 KiB
  modules by path ./node_modules/scheduler/ 31.8 KiB 4 modules
  modules by path ./node_modules/react/ 70.6 KiB 2 modules
  modules by path ./node_modules/react-dom/ 875 KiB 2 modules
  modules by path ./node_modules/css-loader/dist/runtime/*.js 3.78 KiB 2 modules
  ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js 6.67 KiB [built] 
[code generated]
  ./node_modules/object-assign/index.js 2.06 KiB [built] [code generated]
modules by path ./src/ 5 KiB
  modules by path ./src/app/styles/*.less 3.16 KiB
    ./src/app/styles/index.less 385 bytes [built] [code generated]
    ./node_modules/css-loader/dist/cjs.js!./node_modules/less-loader/dist/cjs.js!./src/app
/styles/index.less 2.78 KiB [built] [code generated]
  ./src/renderer/renderer.tsx 373 bytes [built] [code generated]
  ./src/app/components/App.tsx 1.48 KiB [built] [code generated]
webpack 5.21.2 compiled successfully in 4039 ms

But I get Cannot read property 'send' of undefined

enter image description here

If I set in App.tsx :

const sendProxy = window.api.send;

I get the same error and the window is not rendered :

enter image description here

What am I doing wrongly with Typescript and with Electron IPC? Looking forward to your kind help



Solution 1:[1]

Below is my setup based on https://www.electronforge.io, which also adds typings for the exposed api. Hope it helps, even if not a focused answer.

In package.json (using @electron-forge package.json setup, webpack + typescript template), under entryPoints, make sure you have:

"preload": {
    "js": "./src/preload.ts"
}

In src/index.ts where you create your BrowserWindow, use the magic webpack constant to reference the bundled preload script (maybe your preload script didn't get bundled?):

const mainWindow = new BrowserWindow({
    webPreferences: {
      preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY
    }
  });

Contents of src/preload.ts:

import { contextBridge } from "electron";
import api from './api'

contextBridge.exposeInMainWorld("api", api);

src/api/index.ts just exports all features of the api. Example:

import * as myFeature from "./my-feature";

// api exports functions that make up the frontend api, ie that in turn either do IPC calls to main for db communication or use allowed nodejs features like file i/o. 
// Example `my-feature.ts`: 
// export const fetchX = async (): Promise<X[]> => { ... }

export default {
    ...myFeature
}

Typescript 2.9+ can recognise your api functions like api.fetchX by adding a global declaration, e.g. src/index.d.ts (reference):

declare const api: typeof import("./api").default;

...which you need to reference from tsconfig.json:

{ 
  ...
  "files": [
    "src/index.d.ts"
  ]
}

All that done and you should be good to call api.fetchX with typing support (ymmv by IDE) from renderer-side without importing anything. Example App.tsx:

import * as React from 'react'
// do not import api here, it should be globally available

export const App = () => {
  useEffect(() => {
    (async () => {
      const project = await api.fetchX();
      ...
    })();
  }, []);

  return <h1>My App</h1>
}

Solution 2:[2]

Have you required the preload file inside main.ts?

webPreferences: {
  nodeIntegration: false,
  contextIsolation: true,
  preload: path.resolve(path.join(__dirname, "preload.js"))
},

You must place this on the main window.

Depending on your webpack config, there may be one entry point bundle, and you will need to configure an additional webpack output for the preload.js file.

There is an example answer here: How to use preload.js properly in Electron

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 monki32