Create Electron Menu in TypeScript?

For me the problem was the proper role capitalisation:

  { role: 'forceReload' },
  { role: 'toggleDevTools' }

over

  { role: 'forcereload' },
  { role: 'toggledevtools' }

See docs.


I couldn't get the sample that works in JS in TS. The MenuItemConstructorOptions is an interface defined in the electron.d.ts file in the electron package. However I found a workaround by defining the menu entries individually and push them to an empty array. Interestingly the submenu entries inside were accepted and worked without an issue. This is the code that worked:

  function createMenu() {
     const template = [];
     // Edit Menu
     template.push({
        label: 'Edit',
        submenu: [
           { role: 'undo' },
           { role: 'redo' },
           { type: 'separator' },
           { role: 'cut' },
           { role: 'copy' },
           { role: 'paste' },
           { role: 'pasteandmatchstyle' },
           { role: 'delete' },
           { role: 'selectall' }
        ]
     });
     // View Menu
     template.push({
        label: 'View',
        submenu: [
           { role: 'reload' },
           { role: 'forcereload' },
           { role: 'toggledevtools' },
           { type: 'separator' },
           { role: 'resetzoom' },
           { role: 'zoomin' },
           { role: 'zoomout' },
           { type: 'separator' },
           { role: 'togglefullscreen' }
        ]
     });
     // Windown menu
     template.push({
        role: 'window',
        submenu: [{ role: 'minimize' }, { role: 'close' }]
     });
     // Help menu
     template.push({
        role: 'help',
        submenu: [
           {
              label: 'Learn More',
              click() {
                 require('electron').shell.openExternal('https://electron.atom.io');
              }
           }
        ]
     });

     if (process.platform === 'darwin') {
        template.unshift({
           label: app.getName(),
           submenu: [
              { role: 'about' },
              { type: 'separator' },
              { role: 'services', submenu: [] },
              { type: 'separator' },
              { role: 'hide' },
              { role: 'hideothers' },
              { role: 'unhide' },
              { type: 'separator' },
              { role: 'quit' }
           ]
        });

        // Edit menu
        template[1].submenu.push(
           { type: 'separator' },
           { label: 'Speech', submenu: [{ role: 'startspeaking' }, { role: 'stopspeaking' }] }
        );

        // Window menu
        template[3].submenu = [{ role: 'close' }, { role: 'minimize' }, { role: 'zoom' }, { type: 'separator' }, { role: 'front' }];
     }

     menu = Menu.buildFromTemplate(template);
     Menu.setApplicationMenu(menu);
  }

Hope this helps if anybody stumbles over the same issue


I got the same error when I based my menu syntax off Electron's Menu example, which works fine in JavaScript but upsets TypeScript. Unfortunately the MenuItemConstructorOptions[] cast for template as suggested by PhoneixS didn't work for me.

It looks like TypeScript trips on the ternaries in Electron's example and it can't figure out their resulting type. In my case, adding as MenuItemConstructorOptions[] after the closing parentheses enclosing the ternaries made TypeScript realize they're valid submenus after all.

The full Electron example in valid TypeScript:

import { app, Menu, MenuItemConstructorOptions, shell } from "electron"

const isMac = process.platform === 'darwin'

const menu = Menu.buildFromTemplate(
  [
    // { role: 'appMenu' }
    ...(isMac ? [{
      label: app.name,
      submenu: [
        { role: 'about' },
        { type: 'separator' },
        { role: 'services' },
        { type: 'separator' },
        { role: 'hide' },
        { role: 'hideothers' },
        { role: 'unhide' },
        { type: 'separator' },
        { role: 'quit' }
      ]
    }] : []) as MenuItemConstructorOptions[],
    // { role: 'fileMenu' }
    {
      label: 'File',
      submenu: [
        isMac ? { role: 'close' } : { role: 'quit' }
      ] as MenuItemConstructorOptions[]
    },
    // { role: 'editMenu' }
    {
      label: 'Edit',
      submenu: [
        { role: 'undo' },
        { role: 'redo' },
        { type: 'separator' },
        { role: 'cut' },
        { role: 'copy' },
        { role: 'paste' },
        ...(isMac ? [
          { role: 'pasteAndMatchStyle' },
          { role: 'delete' },
          { role: 'selectAll' },
          { type: 'separator' },
          {
            label: 'Speech',
            submenu: [
              { role: 'startSpeaking' },
              { role: 'stopSpeaking' }
            ]
          }
        ] : [
          { role: 'delete' },
          { type: 'separator' },
          { role: 'selectAll' }
        ]) as MenuItemConstructorOptions[]
      ]
    },
    // { role: 'viewMenu' }
    {
      label: 'View',
      submenu: [
        { role: 'reload' },
        { role: 'forceReload' },
        { role: 'toggleDevTools' },
        { type: 'separator' },
        { role: 'resetZoom' },
        { role: 'zoomIn' },
        { role: 'zoomOut' },
        { type: 'separator' },
        { role: 'togglefullscreen' }
      ]
    },
    // { role: 'windowMenu' }
    {
      label: 'Window',
      submenu: [
        { role: 'minimize' },
        { role: 'zoom' },
        ...(isMac ? [
          { type: 'separator' },
          { role: 'front' },
          { type: 'separator' },
          { role: 'window' }
        ] : [
          { role: 'close' }
        ]) as MenuItemConstructorOptions[]
      ]
    },
    {
      role: 'help',
      submenu: [
        {
          label: 'Learn More',
          click: async () => {
            await shell.openExternal('https://electronjs.org')
          }
        }
      ]
    }
  ]
)

Menu.setApplicationMenu(menu)

For me was enough to set the type of template const to Electron.MenuItemConstructorOptions[].

For example:

const template: Electron.MenuItemConstructorOptions[] = [{
        label: 'Edit',
        submenu: [
            { role: 'undo' },
            { role: 'redo' },
            { type: 'separator' },
            { role: 'cut' },
            { role: 'copy' },
            { role: 'paste' },
            { role: 'pasteandmatchstyle' },
            { role: 'delete' },
            { role: 'selectall' }
        ]
    },
    {
        label: 'View',
        submenu: [
            { role: 'reload' },
            { role: 'forcereload' },
            { role: 'toggledevtools' },
            { type: 'separator' },
            { role: 'resetzoom' },
            { role: 'zoomin' },
            { role: 'zoomout' },
            { type: 'separator' },
            { role: 'togglefullscreen' }
        ]
    },
    { role: 'window', submenu: [{ role: 'minimize' }, { role: 'close' }] },
    {
        role: 'help',
        submenu: [{
            label: 'Learn More',
            click() {
                require('electron').shell.openExternal('https://electron.atom.io');
            }
        }]
    }
];