初書:2021/10/14
mac : 11.6
electron : 15.1.2
typescript : 4.4.3
前置き
electronとtypescriptを使ってアプリケーションを開発中のこと。
メニューバーを作成しようと試みたときに起きた出来事。
「型推論が通らない」
ので、それの修正メモ。
該当コード
該当ページMenu | Electron
Electronの日本語公式ページにある「サンプル」コードで問題は起きた。
ちなみにそのコードを、Typescript用に若干書き換えたものがこちら。
import { app, Menu } from ('electron');
const isMac = process.platform === 'darwin';
const template = [
// { 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' }
]
}] : []),
// { role: 'fileMenu' }
{
label: 'ファイル',
submenu: [
isMac ? { role: 'close' } : { role: 'quit' }
]
},
// { 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' }
])
]
},
// { 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' }
])
]
},
{
role: 'help',
submenu: [
{
label: 'Learn More',
click: async () => {
const { shell } = require('electron')
await shell.openExternal('https://electronjs.org')
}
}
]
}
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
requireがimportになって個人的に気に入らなかったセミコロンがついただけ。
ここで下から2行目のMenu.buildFromTemplate(template);
のtemplateにエラーが付きます。
エラーの原因
まずはエラー文はこちら。
型 '({ label: string; submenu: ({ role: string; type?: undefined; label?: undefined; submenu?: undefined; } | { type: string; role?: undefined; label?: undefined; submenu?: undefined; } | { label: string; submenu: { ...; }[]; role?: undefined; type?: undefined; })[]; role?: undefined; } | { ...; })[]' の引数を型 '(MenuItemConstructorOptions | MenuItem)[]' のパラメーターに割り当てることはできません。
型 '{ label: string; submenu: ({ role: string; type?: undefined; label?: undefined; submenu?: undefined; } | { type: string; role?: undefined; label?: undefined; submenu?: undefined; } | { label: string; submenu: { ...; }[]; role?: undefined; type?: undefined; })[]; role?: undefined; } | { ...; }' を型 'MenuItemConstructorOptions | MenuItem' に割り当てることはできません。
型 '{ label: string; submenu: ({ role: string; type?: undefined; label?: undefined; submenu?: undefined; } | { type: string; role?: undefined; label?: undefined; submenu?: undefined; } | { label: string; submenu: { ...; }[]; role?: undefined; type?: undefined; })[]; role?: undefined; }' を型 'MenuItemConstructorOptions | MenuItem' に割り当てることはできません。
型 '{ label: string; submenu: ({ role: string; type?: undefined; label?: undefined; submenu?: undefined; } | { type: string; role?: undefined; label?: undefined; submenu?: undefined; } | { label: string; submenu: { ...; }[]; role?: undefined; type?: undefined; })[]; role?: undefined; }' には 型 'MenuItem' からの次のプロパティがありません: checked, click, commandId, enabled、9 など。ts(2345)
読む気失せるほど長い。
単純にいうと、type
やrole
がstring型になっているが、MenuItemConstructorOptions
内ではストリングリテラル型(限定されたstring型)だよーというエラー。
これは、template
を定義する際に型指定をしていないため、単にstring型として認識されているのが原因。
ということで、template
に型をつける。
const template : Electron.MenuItemConstructorOptions[] = [
// 略
はい、終わりです。エラーの量が。
...(isMac ? [] : []) // 上記コードの一部を切り取っているだけなので、当たり前ですが空の配列ではないです。
この辺が全部エラー吐きます。
解決策を考える
おそらく三項演算子を使っているため、その箇所で型推論していないのが原因なのではないかと予想し、
// 上記と同じエラー
...(isMac ? [] as Electron.MenuItemConstructorOptions[]: [])
// 型 'MenuItemConstructorOptions' には、反復子を返す '[Symbol.iterator]()' メソッドが必要です。ts(2488)
...(isMac ? [] : []) as Electron.MenuItemConstructorOptions[]
いずれもダメ。
仕方ないので外部に切り出す。
let NameSubMenu : Electron.MenuItemConstructorOptions[] = [];
if(isMac){
NameSubMenu = [
{
label: app.name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
}
];
}
const template : Electron.MenuItemConstructorOptions[] = [
// { role: 'appMenu' }
...NameSubMenu ,
// 以下略
これも結局エラーが出るので意味なし。
結論
誰かに頼る。
Create Electron Menu in TypeScript? - Stack Overflow
pushにするといいとかなんとか。
ということで仕方なく全部pushに書き換えた。
const template: Electron.MenuItemConstructorOptions[] = [];
const macMenu: Electron.MenuItemConstructorOptions[] = [
{
label: app.name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'quit' },
],
},
];
if (isMac) {
// { role: 'appMenu' }
template.push(...macMenu);
}
// { role: 'fileMenu' }
template.push({
label: 'ファイル',
submenu: [isMac ? { role: 'close' } : { role: 'quit' }],
});
// { role: 'editMenu' }
const editSubMenu: Electron.MenuItemConstructorOptions[] = [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
];
if (isMac) {
editSubMenu.push(
...([
{ role: 'pasteAndMatchStyle' },
{ role: 'delete' },
{ role: 'selectAll' },
{ type: 'separator' },
{
label: 'Speech',
submenu: [{ role: 'startSpeaking' }, { role: 'stopSpeaking' }],
},
] as Electron.MenuItemConstructorOptions[])
);
} else {
editSubMenu.push(
...([
{ role: 'delete' },
{ type: 'separator' },
{ role: 'selectAll' },
] as Electron.MenuItemConstructorOptions[])
);
}
template.push({
label: 'Edit',
submenu: editSubMenu,
});
// { role: 'viewMenu' }
template.push({
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' },
],
});
// { role: 'windowMenu' }
const windowSubMenu: Electron.MenuItemConstructorOptions[] = [
{ role: 'minimize' },
{ role: 'zoom' },
];
if (isMac) {
windowSubMenu.push(
...([
{ type: 'separator' },
{ role: 'front' },
{ type: 'separator' },
{ role: 'window' },
] as Electron.MenuItemConstructorOptions[])
);
} else {
windowSubMenu.push({ role: 'close' });
}
template.push({
label: 'Window',
submenu: windowSubMenu,
});
template.push({
role: 'help',
submenu: [
{
label: 'Learn More',
click: async () => {
const { shell } = require('electron');
await shell.openExternal('https://electronjs.org');
},
},
],
});
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
いや何かもう少しいい方法が存在している気がしてならない。でもこれ以上思いつきませんでした。
何かいい案あれば教えてください。
それではー