Edited at

clasp が Typescript をサポートした!

以前、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プロジェクトのタイプを選択できるようになった。

standalonewebappapiを選択すると GASのスクリプトだけ作成されるが、docssheetsを選択すると対応するコンテナドキュメントも一緒に作られる。

ここまででできたファイル構成は次の通り。

clasp-ts-sample/

├── .clasp.json
├── node_modules/
├── package-lock.json
├── package.json
├── src
│ └── appsscript.json
└── tslint.json


--rootDirオプションを付けずに作成してしまった場合

うっかり忘れたとか、clasp 1.5.x以下で作成した場合は次の様に.clasp.jsonrootDirを追記する。


.clasp.json

{

"scriptId":"******-***************************************************",
"rootDir": "./src"
}

で、src ディレクトリを作って、clasp pushの対象となるファイルを移動する。

$ mkdir src

$ mv appsscript.json src/

これで--rootDirオプションを使った場合と同じ状態となる。


TypescriptのコードをPUSHしてみる。

claspのリポジトリにあるサンプルをコピーして試してみる。

それが次のコード。alert を使っていた部分は、GASでは動かないので修正している。


Code.ts

// 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されたコードを見てみる。次の様にトランスパイルされている。


Code.gs

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に次のように出力される。

image.png

また、DocumentApp.createして、中に文字列を書き込んでいる部分があるが、

その出力として次のGoogle Docのファイルが Google Driveの中に作成されている。

image.png

おお、感動的に簡単。

これまで、WebpackやBabelを使ってGAS用にトランスパイルしていたが、それらがバカバカしくなるほどだ。

どんどんバージョンアップするWebpackやBabelに追従しようとしてアップデートするとビルドできなくなるようなトラブルからも解放される。

しかも、clasp pushにはwatchモードまである。

次のコマンドを実行しておけば、コードの変更を検知すると再PUSHしてくれる。

$ clasp push --watch


おまけ: VSCode の設定

次の設定を VSCode のUser Settings に追加しておくと、.clasp.jsonappsscript.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を実装できる


参考





  1. claspnpm install -g @google/claspでグローバルにインストールしてもいいが、私は ndenv で複数バージョンのNode.jsをインストールしており、プロジェクトごとにNode.jsのバージョンが異なったりするので、グローバルなインストールは避けている。代わりに、./node_module/.binをPATHに追加してプロジェクトディレクトリにインストールしたコマンドを実行できるようにしている。 



  2. これまで、claspを使ったことがなければ、ログインと、APIの有効化が必要になるが、GAS のGoogle謹製CLIツール claspを参考にしてほしい。