TypeScript でも Hubot のスクリプトが書いてみたい
この記事は さくらインターネット Advent Calendar 2015 14日目の記事です。
みなさんこんばんは。最近業務で TypeScript を使用することが多くなってきました。そこで、1スクリプト当たりのコード量が比較的少なめの Hubot スクリプトを TypeScript で書きなおしたいと思いました。
最近フロントエンジニアになった人、またはエンジニアと一緒にチームでサービスを作るデザイナーを想定して書きました。雰囲気が伝わり、すこしでも興味を持っていもらえると幸いです。
目標
サンプル Hubot スクリプトファイルを TypeScriptに書き直して、それを JavaScript にトランスコンパイルして元の JavaScript のように動作させる。
TypeScript に書き直してみる
なにかとシンプルな方が良さそうなので、hubot-scripts から、xmas.js を題材にしました。
module.exports = function(robot) {
robot.respond(/is it (xmas|christmas)\s?\?/i, function(msg){
var today = new Date();
msg.reply(today.getDate() == 25 && (today.getMonth() + 1) == 12 ? "YES" : "NO");
});
}
(xmas.js は https://github.com/github/hubot-scripts/blob/master/src/scripts/xmas.js より転載しました)
機能としては、Hubot に is it xmas?
などと問いかけると、今日がクリスマスかどうか教えてくれる渋いスクリプトです。社内の Hubot こと mikuru に教えるかもしれない。これを TypeScript化しましょう。
TypeScript化予想図
いきなりですが、予想図っぽいものを予め予想して書いてみます。
import hubot = require("hubot");
module.exports = (robot: hubot.Robot): void => {
robot.respond(/is it (xmas|christmas)\s?\?/i, (msg: hubot.Response) => {
var today: Date = new Date();
msg.reply(today.getDate() === 25 &&
(today.getMonth() + 1) === 12 ? "YES" : "NO");
});
};
エディタには Visual Studio Code を使用しています。TypeScript の構文チェックや補完機能も充実しています。他にも WebStorm, ATOM のパッケージ atom-typescript 等があります。
TypeScript の基礎
座学です。完成予想図に必要な TypeScript の構文中心に学んでいきましょう。
既に TypeScirpt の構文についてご存じの方はこの項目は読み飛ばして下さい。なんと残りはほとんどありません。
基本的な型について
以下は、変数 count
に値 0
を代入しているコードです。
var count: number = 0;
ここで注目してほしいのは、変数 count
の後に続く : number
の部分です。JavaScript には無い文法ですね。最初なので、少し丁寧に説明します。
変数 count
は number型
の変数である、と言うことをコロン区切りの後に型をかいて、対象がどの型を使用するのかを宣言しています(これを型注釈といいます)。
予め count
が number型 であることがわかっているので、数値以外が入れられそうにないということがわかりますね(そして実際、代入しようとすると文法エラーになります)。
// number型の変数に文字列を代入しようとしてエラー
var count: number = "hoge";
この調子で一気に見ていきましょう。
// boolean型の変数 `isShown`
var isShown: boolean = false;
// string型の変数 `name`
var name: string = "nyanchu";
// 要素が string型の配列。配列の型注釈は `[]` の前に要素の型を指定する
var members: string[] = ["kafuka", "chiri", "kaere", "nami"];
// Date型 の `today`
var today: Date = new Date();
また、TypeScript は JavaScript のように、文脈から自動的に型を推論することができます。
// 変数 `count` に number型 `0` を代入する。
// 数値で初期化されているので、number型と推論される
var count = 0;
// string型を代入しようとして文法エラーになる
count = "nyanchu";
TypeScript は JavaScript のスーパーセットであり、言語として JavaScript の仕様を含み、機能を拡張しているにすぎません。
そのため、TypeScript は JavaScript の機能を実装していますし、JavaScript の仕様上の欠陥も引き継いでいます。
関数の戻り値の型
関数は戻り値に型を指定できます。
function nami(): string {
return "Don't call me ordinary!";
}
関数は戻り値がない場合には void
を指定します。
function nami(): void {
alert("Don't call me ordinary!");
}
関数の引数の型
関数の引数に型を指定できます。
function despair(topic: string): string {
return "I'm in despair" + topic;
}
アロー関数式
TypeScript ではアロー関数式が使えます。
var hoge = (): void => {
alert('fuga');
}
関数を function
よりタイプ少なめで書けます。また、大きな特徴として、this
の値を変更しません。関数内やクラス内で thisコンテキストが行方不明になりにくくなります。
CoffeeScript の =>
に似ています。
インターフェース
interface は少し不思議な機能に思えるかもしれません。interface は Class に対して使用して、interface で指定したメソッドやプロパティを Class に実装することを強制します。
使用したい Class のクラス名 に続けて implements インターフェース名
と指定します。
interface Student {
name: string;
phrase: string;
age?: number; // `?` がついたものはオプション扱いになり省略可能
}
class Girl implements Student {
// インターフェース Student で指定した `name`, `phrase` が無いとエラー
// `age?` はオプション扱いだったので無くてもよい
constructor(
public name: string,
public phrase: string
) {
}
public say(): string {
return this.phrase;
}
}
// Girl のインスタンスを作成
var kafuka = new Girl("kafuka", "momoiro.");
// komori の引数に `phrase` がないのでエラー
var komori = new Girl("komori");
// Girls のプロパティには `age` はないのでエラー
var matoi = new Girl("matoi", "Yes. Always.", 18);
また、インターフェースは型注釈にも使用できます。
interface Student {
name: string;
phrase: string;
age?: number;
}
// インターフェース Studentを型注釈に利用
// Student型 の 変数 `student` にオブジェクトを代入
var kaere: Student = { name: "kaere", phrase: "I'll sue!" }
// phrases がオブジェクトのメンバにないのでエラー
var kaede: Student = { name: "kaede" }
// 引数の型にも使える
function getPhrase(student: Student): string {
return student.phrase;
}
Class や interface はメンバに ?
をつけることで、オプション扱いにできます。上記例の場合、age?
がオプションになり、変数に代入する際に省略が許されます。
型定義ファイルの作成
ここまでで目標のスクリプト作成に必要な最低限の文法を学びました。最後に、TypeScript で外部のコンポーネントを利用するために必要な「型定義ファイル」について学びましょう。
アンビエント宣言
TypeScript でも外部のコンポーネントを使用したい場合があります。例えば、jQuery や lodash, 今回で言えば Hubot などです。
外部コンポーネントとして利用する JavaScript には型情報がないので、TypeScriptコンパイラ はこれらをうまく解釈できず、エラーになってしまいます。
そこで、JavaScript ライブラリに型注釈を与えたい場合にはアンビエント宣言が利用できます。また、アンビエント宣言では、関数や変数の実体は含まれません。
declare
キーワードを使用して、型情報を与えます。これにより型注釈を与えることができます。
JavaScriptライブラリの型定義ファイル(.d.ts filesと呼ばれています)はDefinitelyTypedという Github のリポジトリで管理されています。有名なライブラリは型定義ファイルが有志・団体によって作成されていることがあります。
先の jQuery, lodash の型定義ファイルもここで管理されています。
今日現在 Hubot は残念ながら型定義は存在しません・・・。型定義は機能が複雑で大きいほど作成するのが大変になっていきます。
今回は最小限のアンビエント宣言を行います。
アンビエント宣言は、declare の後に本体・初期化部分を省略して定義します。(下記は例)
declare module "someModule" {
export interface Teacher {
age: number;
shout: (method: string, body: any, callback: Function): any;
}
}
では、大幅に駆け足で Hubot の型定義ファイル作成です。ソースコードを読みながら作っているうちに投稿日になってしまいました・・・。
ということで、Hubot のソースから最低限の定義ファイルを作成してみました。
// Type definitions for hubot
// Definitions by: nyanchu
declare module "hubot" {
export interface Robot {
listen(regex: RegExp, options: { [key: string]: any } | Function, callback?: (res: Response) => void): void;
hear(regex: RegExp, options: { [key: string]: any } | Function, callback?: (res: Response) => void): void;
respond(regex: RegExp, options: { [key: string]: any } | Function, callback?: (res: Response) => void): void;
send(user: string, ...string: string[]): void;
reply(user: string, ...string: string[]): void;
}
export interface Response {
match: [string, string, { index: number }, { input: string } ];
send(...strings: string[]): void;
emote(...strings: string[]): void;
reply(...strings: string[]): void;
topic(...strings: string[]): void;
play(...strings: string[]): void;
locked(...strings: string[]): void;
random(items: any[]): any;
finish(): void;
}
}
抜けや手抜きがありますが、ひとまずスクリプトファイルから必要な型を呼び出す事ができました。
スクリプトファイルのトランスコンパイル
いよいよ完成予想図の TypeScript スクリプトを JavaScript にコンパイルしてみましょう。
今回の記事では TypeScript のコンパイルについては省略します。 gulp ファイルの作成等についてはいずれ機会があれば投稿したいと思います。
gulpfile.js に build:ts というタスクを作成しました。TypeScript を JavaScript にトランスコンパイルして build フォルダに書きだすというタスクを担当しています。
コチラが無事コンパイルされた JavaScript ファイルです。さて、うまく動作するでしょうか?
module.exports = function (robot) {
robot.respond(/is it (xmas|christmas)\s?\?/i, function (msg) {
var today = new Date();
msg.reply(today.getDate() === 25 &&
(today.getMonth() + 1) === 12 ? "YES" : "NO");
});
};
$ ./bin/hubot
hubot>
hubot> @hubot is it xmas?
hubot> Shell: NO
できました。おつかれさまでした。クリスマスまであとちょっと!
To be continued?
- 対応するエディタの使い方
- ディレクトリ構成やタスクランナーの内容について
- 型定義ファイル作成過程