Monday, March 18, 2019 / InDesign, ExtendScript, TypeScript

ExtendScript を TypeScript と VisualStudioCode を使って記述する

ExtendScript は今風の JavaScript 記述方法……いわゆる ES6 などが使えない. その代わりに古い JavaScript の記述方法 ES3 などと呼ばれている で書かなければいけない. これはプログラマーにとって、苦痛なだけでなく、生産性も低く、コード品質の維持が難しい. しかし、TypeScript は JavaScript へコンパイルするときにオプションとして --target ES3 を使うことができる. これを使えば、ExtendScript を TypeScript で記述できることになる。

TypeScript で記述することのメリットは、 (1)コンパイル時に型のチェックが行えるようになること、 (2)VisualStudio Code のようなエディタを使って、コードを書いている最中に問題箇所のフィードバックが得られることです。

InDesign 用の ハローワールドな ExtendScript コードを TypeScript で書き直してみる

そこで、以下の簡単なハローワールドなコードをTypeScript書き直してみる.

// add document
var doc = app.documents.add();

// reset page margin
var page = doc.pages.item(0);

// add textFrame
var textFrame = page.textFrames.add();
textFrame.geometricBounds = ["20mm","20mm","40mm","110mm"];
textFrame.contents = 'Hello World!';

まず手始めに var を const に書きかえる.

// add document
const doc = app.documents.add();

// reset page margin
const page = doc.pages.item(0);

// add textFrame
const textFrame = page.textFrames.add();
textFrame.geometricBounds = ["20mm","20mm","40mm","110mm"];
textFrame.contents = 'Hello World!';

VSCode で開くと早速エラーの下線が出ました.

error-cannot-find-name-app

試しに tsc --target ES3 hello-world.ts してみると

error TS2304: Cannot find name 'app'

app という名前のオブジェクトは見つからない、と言われました.

InDesign の ExtendScript の実行環境では、はじめから app が存在しているのですが、 TypeScript のコンパイラはそんなことは知らない. そこで、declare を使って、TypeScript に app が実際にはあることを知らせるためのコードを先頭に追記します.

declare class Application {
    documents;
}
declare var app: Application;

これで VSCode 上のエラー下線も消え、コンパイルしてもエラーなしに js を生成できるようになりました.

ここでは、とりあえず Application というクラスを宣言、そのインスタンスとして app オブジェクトを宣言しています. Application クラスには documents というメンバーを宣言していますが、型を指定しないことで、その先の型チェックがなくなり、うまくコンパイルできるようになっています.

TypeScript を使う本来の目的はコンパイラに型チェックをしてもらうことなので、実践では以下のように細部まで宣言しておくべきでしょう. そうやって完成したコードがこれです.

declare class IndTextFrame {
    geometricBounds
    contents: String
}

declare class IndTextFrames {
    add(): IndTextFrame
}
declare class IndPage {
    textFrames: IndTextFrames
}
declare class IndPages {
    item(index: Number): IndPage
}
declare class IndDocument {
    pages: IndPages
}
declare class IndDocuments {
    add(): IndDocument
}
declare class Application {
    documents: IndDocuments;
}
declare var app: Application;

// add document
const doc = app.documents.add();

// reset page margin
const page = doc.pages.item(0);

// add textFrame
const textFrame = page.textFrames.add();
textFrame.geometricBounds = ["20mm","20mm","40mm","110mm"];
textFrame.contents = 'Hello World!';

このコードを tsc --target ES3 hello-world.ts して hello-world.js を生成すると、 宣言部分は全部削除されて、もともとの ExtendScript と同じコードが生成されます.

declare しているクラス名に Ind プレフィックスを付けたのは、既存のクラス宣言との重複を避けるためです.

まとめ

この例では本体のコードより宣言部分の方がコード分量が多く、まったくうれしくはないのですが、 宣言部分は別ファイルに切り出して使いまわすことができる. 更に言えば、ネット上で探せば、InDesign の宣言ファイル( アンビエント宣言 とか呼ばれているらしい)はすでに公開されているかも. Adobeさんがすでに配布しているかもしれない.(というかまだ配布していないなら配布して… …)

補足

TypeScript をコンパイルするための tsc コマンドはなんとかしてインストールしておいてください.

npm install -g typescript

とか(適当)

また、hello-world.ts を hello-world.jsx に変換するための Makefile 例を以下に.

hello-world.jsx : hello-world.js
    mv $< $@

.INTERMEDIATE: hello-world.js
hello-world.js : hello-world.ts
    tsc --target ES3 $<