224
176

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

clasp が Typescript をサポートした!

Last updated at Posted at 2018-09-06

以前、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.json* に rootDirを追記する。

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

224
176
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
224
176

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?