以前、GAS のGoogle謹製CLIツール claspと題して、@google/claspを紹介した。
このツールと新しい Google Apps Scriptの管理コンソールと Stackdriver Loggingのサポートにより GASの開発、運用環境は格段に使いやすくなった。
その後も開発は進み、なんと v1.5.0 以降で Typescript をサポートしたので、試してみた。
インストール
次のコマンドで、プロジェクトを初期化する1。
$ mkdir clasp-ts-sample
$ cd clasp-ts-sample
$ npm init -y
$ npm install @google/clasp tslint -D
$ npm install @types/google-apps-script -S
$ tslint --init # tslint は必須ではないがグッドマナーとして導入しておこう
Typescriptは明示的にインストールしなくても*@google/clasp*が依存しているのでインストールされる。
@types/google-apps-script によって VSCode等ではコード補完ができるようになる。素晴らしい!
しっかり、SpreadsheetApp
など、GAS固有のクラス群も定義されている。素晴らしい!!
GAS スクリプトの作成
次のコマンドで、GASのファイルをGoogle Driveに作成して、生成されたコードをローカルにpullしている2。
$ clasp create clasp-ts-sample --rootDir ./src
? Clone which script? (Use arrow keys)
❯ standalone
docs
sheets
slides
forms
webapp
api
--rootDir
オプションはつけることをおすすめする。
というのも、--rootDir
オプション付けずに作成すると、clasp push
を実行したときに、node_modules以下のすべてのJSを読み込もうとして、失敗する。
なお、--rootDir
オプションはclasp 1.6.0で追加されたのでclaspが古い場合にはアップデートする。
また、clasp 1.7.0からは作成するGASプロジェクトのタイプを選択できるようになった。
standalone
やwebapp
、api
を選択すると GASのスクリプトだけ作成されるが、docs
やsheets
を選択すると対応するコンテナドキュメントも一緒に作られる。
ここまででできたファイル構成は次の通り。
clasp-ts-sample/
├── .clasp.json
├── node_modules/
├── package-lock.json
├── package.json
├── src
│ └── appsscript.json
└── tslint.json
--rootDir
オプションを付けずに作成してしまった場合
うっかり忘れたとか、clasp 1.5.x以下で作成した場合は次の様に*.clasp.json* に rootDir
を追記する。
{
"scriptId":"******-***************************************************",
"rootDir": "./src"
}
で、src ディレクトリを作って、clasp push
の対象となるファイルを移動する。
$ mkdir src
$ mv appsscript.json src/
これで--rootDir
オプションを使った場合と同じ状態となる。
TypescriptのコードをPUSHしてみる。
claspのリポジトリにあるサンプルをコピーして試してみる。
それが次のコード。alert を使っていた部分は、GASでは動かないので修正している。
// Optional Types
const isDone: boolean = false;
const height: number = 6;
const bob: string = "bob";
const list1: number[] = [1, 2, 3];
const list2: number[] = [1, 2, 3];
enum Color {Red, Green, Blue}
const c: Color = Color.Green;
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean
function showMessage(data: string): void { // Void
Logger.log(data);
}
showMessage("hello");
// Classes
class Hamburger {
constructor() {
// This is the constructor.
}
public listToppings() {
// This is a method.
}
}
// Template strings
const name = "Sam";
const age = 42;
console.log(`hello my name is ${name}, and I am ${age} years old`);
// Rest arguments
const add = (a: number, b: number) => a + b;
const args = [3, 5];
add(...args); // same as `add(args[0], args[1])`, or `add.apply(null, args)`
// Spread operator (array)
const cde = ["c", "d", "e"];
const scale = ["a", "b", ...cde, "f", "g"]; // ['a', 'b', 'c', 'd', 'e', 'f', 'g']
// Spread operator (map)
const mapABC = { a: 5, b: 6, c: 3};
const mapABCD = { ...mapABC, d: 7}; // { a: 5, b: 6, c: 3, d: 7 }
// Destructure map
const jane = { firstName: "Jane", lastName: "Doe"};
const john = { firstName: "John", lastName: "Doe", middleName: "Smith" };
function sayName({firstName, lastName, middleName = "N/A"}) {
console.log(`Hello ${firstName} ${middleName} ${lastName}`);
}
sayName(jane); // -> Hello Jane N/A Doe
sayName(john); // -> Helo John Smith Doe
// Export (The export keyword is ignored)
export const pi = 3.141592;
// Google Apps Script Services
const doc = DocumentApp.create("Hello, world!");
doc.getBody().appendParagraph("This document was created by Google Apps Script.");
// Decorators
function Override(label: string) {
return (target: any, key: string) => {
Object.defineProperty(target, key, {
configurable: false,
get: () => label,
});
};
}
class Test {
@Override("test") // invokes Override, which returns the decorator
public name: string = "pat";
}
const t = new Test();
console.log(t.name); // 'test'
tsc
などを使って事前にトランスパイルする必要はない。
次のコマンドだけで、自動的にトランスパイルして、GASにPUSHしてくれる。
tsconfig.jsonすら用意する必要がない。
JSのコードをPUSHするようにTSのコードをPUSHできるのだ。
もう、JSで、GASを実装する理由が見当たらない。
$ clasp push
GASにPUSHされたコードを見てみる。次の様にトランスパイルされている。
var exports = exports || {};
var module = module || { exports: exports };
var __assign = (this && this.__assign) || Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
// Optional Types
var isDone = false;
var height = 6;
var bob = "bob";
var list1 = [1, 2, 3];
var list2 = [1, 2, 3];
var Color;
(function (Color) {
Color[Color["Red"] = 0] = "Red";
Color[Color["Green"] = 1] = "Green";
Color[Color["Blue"] = 2] = "Blue";
})(Color || (Color = {}));
var c = Color.Green;
var notSure = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean
function showMessage(data) {
Logger.log(data);
}
showMessage("hello");
// Classes
var Hamburger = /** @class */ (function () {
function Hamburger() {
// This is the constructor.
}
Hamburger.prototype.listToppings = function () {
// This is a method.
};
return Hamburger;
}());
// Template strings
var name = "Sam";
var age = 42;
console.log("hello my name is " + name + ", and I am " + age + " years old");
// Rest arguments
var add = function (a, b) { return a + b; };
var args = [3, 5];
add.apply(void 0, args); // same as `add(args[0], args[1])`, or `add.apply(null, args)`
// Spread operator (array)
var cde = ["c", "d", "e"];
var scale = ["a", "b"].concat(cde, ["f", "g"]); // ['a', 'b', 'c', 'd', 'e', 'f', 'g']
// Spread operator (map)
var mapABC = { a: 5, b: 6, c: 3 };
var mapABCD = __assign({}, mapABC, { d: 7 }); // { a: 5, b: 6, c: 3, d: 7 }
// Destructure map
var jane = { firstName: "Jane", lastName: "Doe" };
var john = { firstName: "John", lastName: "Doe", middleName: "Smith" };
function sayName(_a) {
var firstName = _a.firstName, lastName = _a.lastName, _b = _a.middleName, middleName = _b === void 0 ? "N/A" : _b;
console.log("Hello " + firstName + " " + middleName + " " + lastName);
}
sayName(jane); // -> Hello Jane N/A Doe
sayName(john); // -> Helo John Smith Doe
// Export (The export keyword is ignored)
exports.pi = 3.141592;
// Google Apps Script Services
var doc = DocumentApp.create("Hello, world!");
doc.getBody().appendParagraph("This document was created by Google Apps Script.");
// Decorators
function Override(label) {
return function (target, key) {
Object.defineProperty(target, key, {
configurable: false,
get: function () { return label; }
});
};
}
var Test = /** @class */ (function () {
function Test() {
this.name = "pat";
}
__decorate([
Override("test") // invokes Override, which returns the decorator
], Test.prototype, "name");
return Test;
}());
var t = new Test();
console.log(t.name); // 'test'
動作確認
試しにOverrideを実行してみる。
Override以外の関数は実行されないが、関数外の部分は実行される。
もちろんちゃんと動く。
console.log
の出力はStackdriver Loggingに次のように出力される。
また、DocumentApp.create
して、中に文字列を書き込んでいる部分があるが、
その出力として次のGoogle Docのファイルが Google Driveの中に作成されている。
おお、感動的に簡単。
これまで、WebpackやBabelを使ってGAS用にトランスパイルしていたが、それらがバカバカしくなるほどだ。
どんどんバージョンアップするWebpackやBabelに追従しようとしてアップデートするとビルドできなくなるようなトラブルからも解放される。
しかも、clasp push
にはwatchモードまである。
次のコマンドを実行しておけば、コードの変更を検知すると再PUSHしてくれる。
$ clasp push --watch
おまけ: VSCode の設定
次の設定を VSCode のUser Settings に追加しておくと、.clasp.json や appsscript.jsonでもコード補完できるようになる。
{
"json.schemas": [{
"fileMatch": [ "appsscript.json" ],
"url": "http://json.schemastore.org/appsscript"
},
{
"fileMatch": [ ".clasp.json" ],
"url": "http://json.schemastore.org/clasp"
}]
}
まとめ
- @google/clasp が Typescriptをサポートし、一層使えるツールになった。
-
clasp push
で 自動的にトランスパイルしてPUSHしてくれる - クラスはもちろん、テンプレート文字列やスプレッド構文などモダンなコードでGASを実装できる
参考
-
clasp
はnpm install -g @google/clasp
でグローバルにインストールしてもいいが、私はndenv
で複数バージョンのNode.jsをインストールしており、プロジェクトごとにNode.jsのバージョンが異なったりするので、グローバルなインストールは避けている。代わりに、./node_module/.bin
をPATHに追加してプロジェクトディレクトリにインストールしたコマンドを実行できるようにしている。 ↩ -
これまで、
clasp
を使ったことがなければ、ログインと、APIの有効化が必要になるが、GAS のGoogle謹製CLIツール claspを参考にしてほしい。 ↩