gas
TypeScript
clasp

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
$ clasp pull

--rootDirオプションはつけることをおすすめする。
というのも、--rootDirオプション付けずに作成すると、clasp pushを実行したときに、node_modules以下のすべてのJSを読み込もうとして、失敗する。
なお、--rootDirオプションはclasp 1.6.0で追加されたのでclaspが古い場合にはアップデートする。

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

clasp-ts-sample/
├── .clasp.json
├── node_modules/
├── package-lock.json
├── package.json
├── src
│   ├── Code.js
│   └── 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/
$ mv Code.js src/Code.ts

これで--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を参考にしてほしい。