ASIDE を利用して、TypeScript で Google Apps Script (GAS) アプリケーションを開発する際に知っておくと良いこと その1
ASIDE を利用して、TypeScript で Google Apps Script (GAS) アプリケーションを開発する際に知っておくと良いこと その1
English follows Japanese.
まとめ
ASIDE を利用して、TypeScript で GAS アプリケーションを開発する際に知っておくと良いことを今回は2つ紹介する。
- 外部パッケージを使って、アプリケーションコードにパッケージの内容を含めることができる
- 外部パッケージがそのままでは GAS の環境で動かないコードでも「target をダウングレード」することで GAS の環境で動くようにできる
「その1」としているが、全何回になるかは未定である。
例として扱うシステム
- ASIDE を利用して初期設定を行った
- xstate という状態遷移ライブラリを利用して、カウンターの状態遷移を管理する
package.jsonにxstateを追加しているxstateは 5.20.1 で検証
- ローカルでテストを実行することは問題なくできる
- ASIDE に
jestによるテスト環境が標準で含まれているので、それに乗っかる
- ASIDE に
- 作成した関数を含めたアプリケーションを
npm run deployしようとするとエラーになる(後で紹介)- パッケージを含めるための設定
- 環境の互換性の問題
ローカルでは、Node.js の 22.17.1 を利用した。
参考: ASIDE の初期設定
Node.js の実行環境の構築方法については紹介しない。
npx @google/aside init を実行し、プロジェクトを初期化する。こちらも、詳細については紹介しない。
ライセンスを含めるためのファイルやパッケージは不要なので除いた。ここも省略する。
この時点の commit hash が 4929c3cfe50c8248f88d64973015f0375157efe8 である。
外部パッケージの利用
i18next を使った先例が、GAS で多言語対応やってみた として、クラウドエース株式会社の角谷によって紹介されている。基本的にはそれと同じようなことをする。@suzukenz(Kenji Suzuki) による Clasp と babel の組み合わせ例が[Clasp] Google Apps Script で npm install した package を利用するにあるなど、比較的実践されてきた技術ではある。
まずは xstate をインストールする(be41b65253778be4d678ea821b14ec87585bfcfe)。
npm install xstatexstate を利用したコードを src/counter.ts に書いた。また、テストを test/counter.test.ts に書いた(c5a5bf6bf7e8c124f5b2358ca5079230ad712334)。
npm run testで問題なくテストが通る。
src/index.ts に、GAS のメニューから呼び出される関数を追加した(242c61bb8a3f813cc712f59a3bc70cacb7060f1b)。
スプレッドシート側から実行したい場合は、ui 関連のコメントアウトを削除すると良い。
import { createActor } from 'xstate';
import { counterMachine } from './counter';
/**
* スプレッドシートを開いたときに自動実行される GAS エントリポイント
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function onOpen(): void {
const ui = SpreadsheetApp.getUi();
ui.createMenu('Menu')
.addItem('execute counter function', executeCounterFunction.name)
.addToUi();
}
/**
* カウンター関数の実行
*
* GAS メニューから呼び出される
*/
function executeCounterFunction() {
// const ui = SpreadsheetApp.getUi();
const actor = createActor(counterMachine);
actor.start();
actor.send({ type: 'INCREMENT' });
let snapshot = actor.getSnapshot();
console.log(snapshot.context.count);
// ui.prompt(`Current count: ${snapshot.context.count}`);
actor.send({ type: 'INCREMENT' });
snapshot = actor.getSnapshot();
console.log(snapshot.context.count);
// ui.prompt(`Current count: ${snapshot.context.count}`);
actor.stop();
}
この時点で npm run deploy を実行すると、以下のような出力が得られる。
> type-script-gas-downgrade@0.0.0 deploy
> npm run lint && npm run test && npm run build && ncp .clasp-dev.json .clasp.json && clasp push -f
> type-script-gas-downgrade@0.0.0 lint
> eslint --fix --no-error-on-unmatched-pattern src/ test/
=============
WARNING: You are currently running a version of TypeScript which is not officially supported by @typescript-eslint/typescript-estree.
You may find that it works just fine, or you may not.
SUPPORTED TYPESCRIPT VERSIONS: >=3.3.1 <5.2.0
YOUR TYPESCRIPT VERSION: 5.8.3
Please only submit bug reports when using the officially supported version.
=============
(node:337893) [DEP0180] DeprecationWarning: fs.Stats constructor is deprecated.
(Use `node --trace-deprecation ...` to show where the warning was created)
> type-script-gas-downgrade@0.0.0 test
> jest test/ --passWithNoTests --detectOpenHandles
PASS test/counter.test.ts
counterMachine
✓ should have initial count 0 (4 ms)
✓ should increment count on INCREMENT (1 ms)
✓ should reset count to 0 on RESET (1 ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 1.063 s, estimated 2 s
Ran all test suites matching test/.
> type-script-gas-downgrade@0.0.0 build
> npm run clean && npm run bundle && ncp appsscript.json dist/appsscript.json
> type-script-gas-downgrade@0.0.0 clean
> rimraf build dist
> type-script-gas-downgrade@0.0.0 bundle
> rollup --no-treeshake -c rollup.config.mjs
src/index.ts → dist...
(!) Unresolved dependencies
https://rollupjs.org/troubleshooting/#warning-treating-module-as-external-dependency
xstate (imported by "src/counter.ts" and "src/index.ts")
created dist in 1s
Syntax error: SyntaxError: Cannot use import statement outside a module line: 1 file: index.gs最後の行:
Syntax error: SyntaxError: Cannot use import statement outside a module line: 1 file: index.gsの部分がエラーである。生成された dist/index.js を見ると、以下のような行がファイル冒頭に存在している
import { createMachine, assign, createActor } from 'xstate';GAS では、アップデートされたファイルがすべて読み込まれるが、import の仕組みはないので、利用ライブラリの内容をすべて含める必要がある。
@rollup/plugin-node-resolve を使うと、依存ライブラリで、node_modules 以下にあるものを、rollup 時に一緒に含めてくれる。
(多分 --save-dev オプションを付けた方が良いけど)以下を実行して、プラグインを追加する(6fac9473e58016f17456660636cfb9ecb4744fba)。
npm install @rollup/plugin-node-resolveその後、rollup.config.mjs に import nodeResolve from '@rollup/plugin-node-resolve'; と nodeResolve(), を追加する(38d5dee2e4df3e6adf737abff136f3ce420c54fe)。
すると、npm run deploy を実行したときに、出力の最後の部分が以下のようになる。
src/index.ts → dist...
created dist in 1.9s
Syntax error: ParseError: Unexpected token = line: 984 file: index.gs内容の追加自体はできているが、GAS 内の V8 が特定の文法に対応していない。ちなみに、984 行目は
reportError ||= !errorListener;である。
ひとつ目の話題、「外部パッケージを使って、アプリケーションコードにパッケージの内容を含めることができる」については解決したが、対応している JavaScript の(文法)バージョンの問題が残っている。
target をダウングレード
ES3, ES5, ES2015 …が何なのかについては、ここでは触れない。
まず、「自分でコントロールできる範囲のコード」については、tsconfig.json の target を es2019 等に指定し、TypeScript から JavaScript に変換する段階でダウングレードすれば解決する。
2025-07 の時点で、ASIDE が提供する tsconfig.json を見ると、"target": "es2020", となっている。これを 2019 にすれば良いというわけである。
上記の手法は、株式会社CureApp の @shinout(しん すずき) によるclasp pushでエラーが起きたらtsconfig.jsonのtargetを下げよ。や、ゆでによるGASにおけるアプリケーション制作手法の見直しで既に紹介されている。
今回の問題はそれでは解決しない。
xstate のコードは、「外部」のパッケージであり、tsconfig.json の target を変更しても、xstate のコードは変わらない。つまり、xstate のコードが GAS の V8 が対応していない文法を使っているために、エラーが発生している。
バンドラの rollup の設定で、babel を追加し、preset に node のバージョンを指定することで、dist に出力されるコードの、JavaScript のバージョンを下げることができる。
具体的には以下のことを行うと良い(d8c8db3685d4ee362a7d14a301959ccee45dbd6f):
npm install @rollup/plugin-babel @babel/preset-envrollup.config.mjs に以下のように babel の設定を追加する。Node.js 16系に上げてしまうとダメだったので、14.21 を指定した(8ea27ac9ca8cf510b42c02f597c778ca9121c6ef)。
import babel from "@rollup/plugin-babel";
// ...
plugins: [
// ...
babel({
babelHelpers: 'bundled',
presets: [
[
'@babel/preset-env',
{
targets: {
node: '14.21'
},
}
]
]
}),
]こうした後に npm run deploy を実行すると、出力の最終部分が以下のように変化する。
src/index.ts → dist...
created dist in 2.6s
Pushed 2 files.
└─ dist/appsscript.json
└─ dist/index.jsこれで、dist/index.js の内容が GAS の V8 が対応している文法に変換されていて、その内容でスクリプトが更新された。
参考文献
- GAS で多言語対応やってみた 角谷
- [Clasp] Google Apps Script で npm install した package を利用する @suzukenz(Kenji Suzuki)
- clasp pushでエラーが起きたらtsconfig.jsonのtargetを下げよ。 @shinout(しん すずき)
- GASにおけるアプリケーション制作手法の見直し ゆで
Essential Tips for Developing Google Apps Script (GAS) Applications with TypeScript and ASIDE: Part 1
Summary
This article covers two key techniques for developing Google Apps Script (GAS) applications with TypeScript using ASIDE:
- Bundling External Packages: How to incorporate npm packages into your application code.
- Ensuring Compatibility: How to make external packages compatible with the GAS environment by transpiling them to an older JavaScript target, even if they don’t work out-of-the-box.
Although this is labeled as "Part 1", the total number of parts is undecided.
Example System
- The project was initialized using ASIDE
- We’ll use the
xstatelibrary to manage the state transitions of a simple counterxstateis added topackage.json- Verified with xstate version 5.20.1
- Local tests run successfully
- We’ll leverage the default
jesttesting environment provided by ASIDE
- We’ll leverage the default
- Attempting to deploy the application with
npm run deployresults in an error (which we will analyze later). This is due to:- The need for specific settings to bundle packages.
- Environment compatibility issues.
Locally, Node.js version 22.17.1 was used.
Reference: Initial Setup of ASIDE
A guide on setting up the Node.js runtime environment is beyond the scope of this article.
The project is initialized by running npx @google/aside init. We will also skip the detailed explanation of this process.
Files and packages related to licensing were removed as they are not needed for this example.
The commit hash at this point is 4929c3cfe50c8248f88d64973015f0375157efe8.
Using External Packages
A similar approach was demonstrated by Kadotani of Cloud Ace Inc. in their article "GAS で多言語対応やってみた," which uses i18next. This technique of bundling dependencies is relatively common, with other examples like @suzukenz’s (Kenji Suzuki) article on combining Clasp and Babel, "[Clasp] Google Apps Script で npm install した package を利用する](https://qiita.com/suzukenz/items/dbe13d5f8884752a37f8)".
First, install xstate (be41b65253778be4d678ea821b14ec87585bfcfe):
npm install xstateWe’ll write the xstate logic in src/counter.ts and its corresponding tests in test/counter.test.ts (c5a5bf6bf7e8c124f5b2358ca5079230ad712334). Running npm run test confirms the tests pass successfully.
Next, we add a function to src/index.ts that will be called from the GAS menu (242c61bb8a3f813cc712f59a3bc70cacb7060f1b). To run this from the spreadsheet UI, uncomment the lines related to the ui object.
import { createActor } from 'xstate';
import { counterMachine } from './counter';
/**
* GAS entry point automatically executed when opening the spreadsheet
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function onOpen(): void {
const ui = SpreadsheetApp.getUi();
ui.createMenu('Menu')
.addItem('execute counter function', executeCounterFunction.name)
.addToUi();
}
/**
* Execute counter function
*
* Called from the GAS menu
*/
function executeCounterFunction() {
// const ui = SpreadsheetApp.getUi();
const actor = createActor(counterMachine);
actor.start();
actor.send({ type: 'INCREMENT' });
let snapshot = actor.getSnapshot();
console.log(snapshot.context.count);
// ui.prompt(`Current count: ${snapshot.context.count}`);
actor.send({ type: 'INCREMENT' });
snapshot = actor.getSnapshot();
console.log(snapshot.context.count);
// ui.prompt(`Current count: ${snapshot.context.count}`);
actor.stop();
}
> type-script-gas-downgrade@0.0.0 deploy
> npm run lint && npm run test && npm run build && ncp .clasp-dev.json .clasp.json && clasp push -f
> type-script-gas-downgrade@0.0.0 lint
> eslint --fix --no-error-on-unmatched-pattern src/ test/
=============
WARNING: You are currently running a version of TypeScript which is not officially supported by @typescript-eslint/typescript-estree.
You may find that it works just fine, or you may not.
SUPPORTED TYPESCRIPT VERSIONS: >=3.3.1 <5.2.0
YOUR TYPESCRIPT VERSION: 5.8.3
Please only submit bug reports when using the officially supported version.
=============
(node:337893) [DEP0180] DeprecationWarning: fs.Stats constructor is deprecated.
(Use `node --trace-deprecation ...` to show where the warning was created)
> type-script-gas-downgrade@0.0.0 test
> jest test/ --passWithNoTests --detectOpenHandles
PASS test/counter.test.ts
counterMachine
✓ should have initial count 0 (4 ms)
✓ should increment count on INCREMENT (1 ms)
✓ should reset count to 0 on RESET (1 ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 1.063 s, estimated 2 s
Ran all test suites matching test/.
> type-script-gas-downgrade@0.0.0 build
> npm run clean && npm run bundle && ncp appsscript.json dist/appsscript.json
> type-script-gas-downgrade@0.0.0 clean
> rimraf build dist
> type-script-gas-downgrade@0.0.0 bundle
> rollup --no-treeshake -c rollup.config.mjs
src/index.ts → dist...
(!) Unresolved dependencies
https://rollupjs.org/troubleshooting/#warning-treating-module-as-external-dependency
xstate (imported by "src/counter.ts" and "src/index.ts")
created dist in 1s
Syntax error: SyntaxError: Cannot use import statement outside a module line: 1 file: index.gsAt this point, running npm run deploy fails with the following error at the end of the output:
Syntax error: SyntaxError: Cannot use import statement outside a module line: 1 file: index.gsLooking at the generated dist/index.js, we find this line at the top:
import { createMachine, assign, createActor } from 'xstate';The GAS runtime loads all script files but doesn’t support ES module import statements. Therefore, all library code must be bundled into a single file.
To solve this, we can use @rollup/plugin-node-resolve. This Rollup plugin finds dependencies in node_modules and includes them in the final bundle.
Install the plugin (maybe it’s recommended to use the --save-dev option) (6fac9473e58016f17456660636cfb9ecb4744fba):
npm install @rollup/plugin-node-resolveThen, add import nodeResolve from '@rollup/plugin-node-resolve'; and nodeResolve(), to rollup.config.mjs(38d5dee2e4df3e6adf737abff136f3ce420c54fe).
Now, running npm run deploy again, we encounter a different error:
src/index.ts → dist...
created dist in 1.9s
Syntax error: ParseError: Unexpected token = line: 984 file: index.gsThe package is now bundled, but we’ve hit another issue: the GAS V8 runtime doesn’t support some modern JavaScript syntax. The problematic code on line 984 is a logical OR assignment (||=):
reportError ||= !errorListener;We’ve solved our first problem: bundling external packages. However, we now face a new challenge related to JavaScript syntax compatibility.
Downgrading the Target
A detailed explanation of JavaScript versions (ES3, ES5, ES2015, etc.) is outside the scope of this article.
For your own source code, this type of issue can typically be resolved by setting the target in tsconfig.json to an older version like es2019. This instructs the TypeScript compiler to output JavaScript compatible with that version. As of 2025-05, the tsconfig.json provided by ASIDE defaults to "target": "es2020", so simply changing this would be the solution in that case. This method has been previously described in articles: "clasp pushでエラーが起きたらtsconfig.jsonのtargetを下げよ。" by @shinout(しん すずき) and "GASにおけるアプリケーション制作手法の見直し" by Yude.
However, this approach won’t solve our current problem.
The xstate package is an external dependency. Changing our project’s tsconfig.json doesn’t affect the code within node_modules. The error originates from xstate itself using modern syntax that the GAS V8 runtime doesn’t support.
The solution is to use Babel with Rollup. By adding @rollup/plugin-babel and configuring it with @babel/preset-env, we can transpile not only our code but also our dependencies to an older, more compatible version of JavaScript.
Specifically, you’ll need to install the following packages (d8c8db3685d4ee362a7d14a301959ccee45dbd6f):
npm install @rollup/plugin-babel @babel/preset-envThen, add the Babel plugin configuration to rollup.config.mjs. Note that targeting Node.js 16 didn’t resolve the issue in my testing; Node.js 14.21 was required for compatibility (8ea27ac9ca8cf510b42c02f597c778ca9121c6ef).
import babel from "@rollup/plugin-babel";
// ...
plugins: [
// ...
babel({
babelHelpers: 'bundled',
presets: [
[
'@babel/preset-env',
{
targets: {
node: '14.21'
},
}
]
]
}),
]With this configuration in place, running npm run deploy finally succeeds:
src/index.ts → dist...
created dist in 2.6s
Pushed 2 files.
└─ dist/appsscript.json
└─ dist/index.jsThis process transpiles the entire bundle, including xstate, into syntax compatible with the GAS V8 runtime. The script is now successfully deployed and updated.
References
- Tried Multilingual Support in GAS GAS で多言語対応やってみた Kadotani 角谷
- [Clasp] Using npm installed packages in Google Apps Script [Clasp] Google Apps Script で npm install した package を利用する @suzukenz(Kenji Suzuki)
- If you get an error with clasp push, lower the target in tsconfig.json. clasp pushでエラーが起きたらtsconfig.jsonのtargetを下げよ。 @shinout(しん すずき)
- Review of application development methods in GAS GASにおけるアプリケーション制作手法の見直し Yude ゆで

ディスカッション
コメント一覧
まだ、コメントがありません