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

3

10

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

Richardricharda answered 11/2, 2021 at 10:32 Comment(1)
where you able to solve this?Brigette
L
23

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 x = await api.fetchX();
      ...
    })();
  }, []);

  return <h1>My App</h1>
}
Luo answered 11/2, 2022 at 10:18 Comment(6)
Thanks for the super detailed answer. I'm assuming when you say we can call api.fetchX anywhere, that's on the main process, not in the renderer/client-side context? To access the api from the client-side, you need to use window.api.fetchX, is my understanding correct?Ody
api is actually available from renderer context, courtesy of exposeInMainWorld('api', api), despite this method being weirdly named. Also, make sure not to import any of the features making up api from source code on renderer side. I'll add a bit to the answer.Luo
Ahh you're right, api is indeed available! Thank you for the little bit you've added to your answer.Ody
Also, just in case you want to update your answer or anyone else runs into this problem. In the global type declaration, since in your example we are importing the default export from ./api, I needed to make the type declaration declare const api: typeof import("./api").defaultOdy
perfect, thanks @hkennyv, will updateLuo
I've done it this way but my render files can't locate the type from this kind of declarationMolybdous
D
1

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

Dorrisdorry answered 23/3, 2021 at 7:49 Comment(0)
E
1

One works for me is using index.d.ts file in src folder. I created my electron app using electronForge with Webpack + Typescript. I used below command for creation

yarn create electron-app my-new-app --template=webpack-typescript

First you need to export api you created. My preload script would looks like below with exported api (electronAPI)

import { contextBridge, ipcRenderer } from 'electron';

export const electronAPI = {
  getProfile: () => ipcRenderer.invoke('auth:get-profile'),
  logOut: () => ipcRenderer.send('auth:log-out'),
  getPrivateData: () => ipcRenderer.invoke('api:get-private-data'),
};

process.once("loaded", () => {
  contextBridge.exposeInMainWorld('electronAPI', electronAPI);
});

Next create index.d.ts inside src folder like this

import { electronAPI } from "./main/preload";

declare global {
    interface Window {electronAPI: typeof electronAPI}
}

Those are the only changes I have made to get TS support for preload scripts. Since this is created by electronForge forge.config.ts and tsconfig.json files got automatically created. So I won't show them here since they haven't been changed.

Excommunicative answered 14/9, 2023 at 6:51 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.