ASIDE を利用して、TypeScript で Google Apps Script (GAS) アプリケーションを開発する際に知っておくと良いこと その1

ASIDE を利用して、TypeScript で Google Apps Script (GAS) アプリケーションを開発する際に知っておくと良いこと その1

English follows Japanese.

まとめ

ASIDE を利用して、TypeScript で GAS アプリケーションを開発する際に知っておくと良いことを今回は2つ紹介する。

  1. 外部パッケージを使って、アプリケーションコードにパッケージの内容を含めることができる
  2. 外部パッケージがそのままでは GAS の環境で動かないコードでも「target をダウングレード」することで GAS の環境で動くようにできる

「その1」としているが、全何回になるかは未定である。

例として扱うシステム

  • ASIDE を利用して初期設定を行った
  • xstate という状態遷移ライブラリを利用して、カウンターの状態遷移を管理する
    • package.jsonxstate を追加している
    • xstate は 5.20.1 で検証
  • ローカルでテストを実行することは問題なくできる
    • ASIDE に jest によるテスト環境が標準で含まれているので、それに乗っかる
  • 作成した関数を含めたアプリケーションを 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 xstate

xstate を利用したコードを 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.mjsimport 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.jsontargetes2019 等に指定し、TypeScript から JavaScript に変換する段階でダウングレードすれば解決する。

2025-07 の時点で、ASIDE が提供する tsconfig.json を見ると、"target": "es2020", となっている。これを 2019 にすれば良いというわけである。

上記の手法は、株式会社CureApp の @shinout(しん すずき) によるclasp pushでエラーが起きたらtsconfig.jsonのtargetを下げよ。や、ゆでによるGASにおけるアプリケーション制作手法の見直しで既に紹介されている。

今回の問題はそれでは解決しない。

xstate のコードは、「外部」のパッケージであり、tsconfig.jsontarget を変更しても、xstate のコードは変わらない。つまり、xstate のコードが GAS の V8 が対応していない文法を使っているために、エラーが発生している。

バンドラの rollup の設定で、babel を追加し、preset に node のバージョンを指定することで、dist に出力されるコードの、JavaScript のバージョンを下げることができる。

具体的には以下のことを行うと良い(d8c8db3685d4ee362a7d14a301959ccee45dbd6f):

npm install @rollup/plugin-babel @babel/preset-env

rollup.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 が対応している文法に変換されていて、その内容でスクリプトが更新された。

参考文献


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:

  1. Bundling External Packages: How to incorporate npm packages into your application code.
  2. 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 xstate library to manage the state transitions of a simple counter
    • xstate is added to package.json
    • Verified with xstate version 5.20.1
  • Local tests run successfully
    • We’ll leverage the default jest testing environment provided by ASIDE
  • Attempting to deploy the application with npm run deploy results 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 xstate

We’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.gs

At 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.gs

Looking 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-resolve

Then, 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.gs

The 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-env

Then, 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.js

This process transpiles the entire bundle, including xstate, into syntax compatible with the GAS V8 runtime. The script is now successfully deployed and updated.

References