23

Just started a simple Electron app using TypeScript and I'm trying to get my custom menu setup. I followed the example in JS, but the line

menu = Menu.buildFromTemplate(template); fails to compile with the error:

main.ts(109,35): error TS2345: Argument of type '({ label: string; submenu: ({ role: string; } | { type: string; })[]; } | { role: string; submenu...' is not assignable to parameter of type 'MenuItemConstructorOptions[]'.

I must be missing something. Couldn't find a type "MenuItemConstructorOptions anywhere (but I might have looked in the wrong places). My full code for main.ts:

import { app, BrowserWindow, screen, Menu } from 'electron';
import * as path from 'path';

let win, menu;

function createWindow() {
    const electronScreen = screen;
    const size = electronScreen.getPrimaryDisplay().workAreaSize;

    win = new BrowserWindow({
        x: 0,
        y: 0,
        width: size.width,
        height: size.height
    });

    // and load the index.html of the app.
    win.loadURL('file://' + __dirname + '/index.html');
    win.webContents.openDevTools();
    win.on('closed', () => {
        win = null;
    });
}

function createMenu() {
    const template = [{
            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');
                }
            }]
        }
    ];
    menu = Menu.buildFromTemplate(template);
    Menu.setApplicationMenu(menu);
}

try {
    app.on('ready', function() {
        createWindow();
        createMenu();
    });
    app.on('window-all-closed', () => {
        if (process.platform !== 'darwin') {
            app.quit();
        }
    });

    app.on('activate', () => {
        if (win === null) {
            createWindow();
        }
    });
} catch (e) {
    throw e;
}
gervais.b
  • 2,294
  • 2
  • 22
  • 46
stwissel
  • 20,110
  • 6
  • 54
  • 101

6 Answers6

36

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');
            }
        }]
    }
];
PhoneixS
  • 10,574
  • 6
  • 57
  • 73
7

For me the problem was the proper role capitalisation:

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

over

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

See docs.

moonwave99
  • 21,957
  • 3
  • 43
  • 64
5

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

stwissel
  • 20,110
  • 6
  • 54
  • 101
5

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)
jkmartindale
  • 523
  • 2
  • 9
  • 22
2

In the MenuItemConstructorOptions interface, the submenu property is defined with a union type. So the property needs to be cast to the MenuItemConstructorOptions array in order for the push operator to be recognized:

(windowMenu.submenu as MenuItemConstructorOptions[]).push(
  {
    type: 'separator',
  },
  {
    label: 'Bring All To Front',
    role: 'front'
  }
);
0

You have a mistake / typo I guess here:

{ type: 'separator' },

TypeScript cannot resolve the type because of unknown type property (should be role according to your other inputs)

smnbbrv
  • 23,502
  • 9
  • 78
  • 109
  • The official documentation: https://github.com/electron/electron/blob/master/docs/api/menu-item.md says "type" is an optional property with allowed values of normal, separator, submenu, checkbox or radio – stwissel Aug 22 '17 at 07:44
  • @stwissel that could be true. But the typescript does not understand something. And `MenuItemConstructorOptions` type definitions should be checked here, not the documentation. TypeScript cannot match your array data type with the definition. Could you post the definition of `MenuItemConstructorOptions` please? – smnbbrv Aug 22 '17 at 09:21
  • That's the funny thing: I couldn't find the definition for `MenuItemConstructorOptions` anywhere. Seems to be missing from the typings. Guess I have to create it – stwissel Aug 22 '17 at 10:50
  • I am pretty sure it exists. Most likely it is created from scratch during every build (that looks like that if you look into package.json scripts). Anyway TypeScript knows of it otherwise it would not give an error `is not assignable to parameter of type 'MenuItemConstructorOptions[]'` – smnbbrv Aug 22 '17 at 12:53