TypeScriptを導入する上でのポイントまとめ
こんにちわ。セイカイです。
JavaScriptは簡単に動いて取り回しの柔軟な言語ですが、開発のスケールを考えると中々辛いところがありますね。
というところで、今回はTypeScriptの導入に関するお話です。
実際導入するにあたってのポイントなどを紹介したいと思います。
目次
目次
導入にあたって
- 導入の目的
- メリット
- デメリット
導入する上でのポイント
- 環境構築
- 構成規則
- 外部ライブラリ
- Docs/規約
- その他
まとめ
- まとめ
導入の目的
AltJSの導入目的は、JavaScriptはclassが無かったり、自由度が高すぎたり、書き方が統一し難かったりと大きな規模での開発に適用し辛いという部分が大きなところではないでしょうか。
私がTypeScriptを使い始めた理由も上記同等となります。
尚、AltJSの中でTypeScriptを選んだ理由としては下記のようなものになります。
- 既存のJavaScriptとの親和性
- JavaScriptからTypeScriptで作成したコードの再利用がしやすい
- 将来的に開発環境が一番充実しそう
使用想定
- 使用者:AS3やJava等の経験者
- 条件1:ES5に限定出来る環境
- 条件2:JavaScriptからTypeScriptで作成したコードの再利用を行う可能性がある
- 条件3:ブラウザ以外の環境
- 条件4:複数人での開発
今回、導入したケースは弊社Herlock上での社内開発をベースとしていますので、上記の様な想定となりました。
少し特殊な条件下を想定していますが、通常のブラウザ開発においても、TypeScriptの導入方法としては、そう変わらないかと思います。
メリット
期待出来るメリットは下記の様になりますね。
- コンパイル時エラー等によるバグの早期発見
- 型定義による可読性の向上
- ファイル分割性能の向上
- 言語機能による複雑さの解消
- 言語側で補える部分が増えコード規約がシンプルに
- 言語側で強制される部分が多く開発時にルールの策定が容易
- IDEのコード補完等による恩恵が大きくなる
デメリット
当然デメリットもあります。
想定されるのは下記の様な部分ではないでしょうか。
- 環境構築の手間
- コンパイルの手間
- コード量の増大
- シンタックスを覚える学習コスト
もちろん、ある程度はIDE等で補える部分もあります。
環境構築
TypeScript自体の導入
TypeScriptのコンパイル環境はnodejsで動いていますので、nodejsを入れる必要があります。
nodejsがインストールされていれば、TypeScriptはnpmで管理されていますので、下記一行でインストール可能です。
1 |
[sourcecode lang="shell"]npm install -g typescript[/sourcecode] |
これで、tscコマンドが使用可能なります。
tscコマンドはTypeScriptをコンパイルするコマンドです。
1 |
[sourcecode lang="shell"]tsc helloworld.ts[/sourcecode] |
ちなみに、現在TypeScriptの最新バージョンは0.9.1となっています。
IDEの選定
候補としては、WebStorm(PHPStorm,Intellij IDEA)、Eclipse、VisualStudio、FlashDevelop、SublimeText等があります。
WindowsではVisualStudioが本家だけあって環境としては良さそうな気がしますが、自分の場合はMac環境ですので、今迄使っていたStormを使用しています。
Stormの不足点
使っている感想としては、完全にサポート出来ていない部分と、細かい設定が出来ない等不十分な部分は多々ありますが、今後解消されるのではないでしょうか。
一応不自由な部分をまとめてみました。
- tscコマンドではエラーにならないが、IDE上ではエラーとして表示されるものがある
- 関数をfunctionとして定義した際のthisに関するエラー表示
- ジェネリクスに関する代入時のエラー表示
- interface宣言の中で他の型を認識出来ないケースがある
- デフォルトlib.d.tsが補完候補から除外出来ない
- 外部ファイルに定義クラス使用時に自動でimportされない
- エラーの細かい制御等言語設定がない
私が知らないだけで、本当は出来るものもあるかもですが、そんなところです。
コードを書くだけなら、そこまで不自由はないですが、エラー等は気分的に気持ち悪いので今後に期待ですね。
構成規則
クラスの定義
今回は、使用することを想定されるメンバーの経験等からASやJava等に構成に近い形にすることを考えたので、moduleという概念をnamespaceに置き換えて考えました。
あとは、言語的に足りない部分を規約等でどう補うかという考え方です。
構文的には下記のような対応としています。
- package(namespace) => module
- class => class
- interface => interface
- import => import(+reference)
- public => public
- protected => public(命名規則とDocで補完)
- private => private
protectedが無いのが割と煩わしい部分ですが、ひとまずコメントと命名規則で補完ということにしています。
下記がコード例となります。
雰囲気的には、as3と似た様な感じになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
[sourcecode lang="java"]/// <reference path="Abstract.ts" /> /// <reference path="../other/Foo.ts" /> /** * @namespace sample.command */ module sample.command { import Foo = sample.other.Foo; /** * @class Hoge * @extends hlk.command.Abstract * @constructor * @param {any} param */ export class Hoge extends Abstract { /** * _fuga * * @type Foo * @property _fuga */ private _fuga:Foo; /** * _name * * @protected * @type string * @property _name */ public _name:string; /** * id * * @protected * @type number * @property id */ public id:number; /** * @constructor * @param {any} param */ constructor(param:any) { super(); this._fuga = new Foo(param); this._name = 'abc'; this.id = 1; } /** * @method execute * @param {any} [data] */ execute(data?:any) { //any code } } }[/sourcecode] |
※constructorコメントを二カ所に書いているのは、yuidocの都合上になります。
※同一モジュールツリー内のクラスに関してはreferenceの記述オンリーで参照可能です。
ファイルの構成
ファイルの構成は、原則1ファイルクラスとして定義することを前提としました。
上記の例ですと下記のようになります。
ファイルの分割方式
ファイルの分割方式は、直感的な記述を重視してamdによるmodule化ではなく、コンパイル時結合を前提としています。
module規則は同一命が重複しても問題なくコンパイル可能です。
tscコマンドに-oオプションを渡すことで、referenceの記述を元に結合したファイルをコンパイルします。
上記の例ですと、下記のようなコマンドでhoge.jsに結合された結果が出力されます。
1 |
[sourcecode lang="shell"]tsc sample/Hoge.ts -o hoge.js[/sourcecode] |
注意点としては、ライブラリ層のjsファイルを分割したい等の場合は、使用側で直接tsファイルを参照すると、全て結合されていましますので、下記の様にライブラリ側でd.tsファイルを出力して、使用側はそれを参照するのが良いでしょう。
1 |
[sourcecode lang="shell"]tsc -d sample/Hoge.ts -o hoge.js[/sourcecode] |
これで、hoge.jsとhoge.d.ts(定義ファイル)が出力されるようになります。
外部ライブラリ
外部ライブラリの読み込みは、実行時に関しては単純にjs本体を読み込みます。
コード記述時は定義ファイル(d.ts)を読み込んで定義を参照します。
有名なライブラリの定義ファイルは下記に纏まっています。
https://github.com/borisyankov/DefinitelyTyped
Docからの自動生成は以前の記事を参照してください。
デフォルトライブラリ
弊社のHerlock等、ブラウザ意外で動くことを前提とした場合、ブラウザ用の定義は不要となります。
この場合。代替の定義ファイルを用意してやれば良いのですが、定義が重複するとコンパイラに怒られるので、定義ファイルの中に下記の一行を足してあげます。
1 |
[sourcecode lang="xml"]/// <reference no-default-lib="true"/>[/sourcecode] |
これでデフォルトのlib.d.tsは参照されなくなります。
※ただしIDE自体の機能としては勝手に読み込まれるケースがありますが、コンパイラが通ればとりあえずはOKです
尚、以前はtscコマンドにnolibオプションがありましたが、最新版では不要のようです。
Docs/規約
クラス定義の項目でも書きましたが、TypeScriptはまだ発展途上なので「言語的に足りない部分を規約とDocで補う」という部分は重要です。
現状ですと、アクセス修飾子が不十分なのでprotectedは命名規則とDocコメントで補うのが無難かと思います。
Doc
ドキュメントツールは、とりあえずyuidocを使っています。
理由としては、軽く調べると「yuidocが良いんじゃないか」的な流れだったので、という安直な感じですが・・・。
クセとしては、割とちゃんとコメント書かないと出力されない、namspace直下の関数等は考慮していない、コンストラクタの定義はクラス定義と同じ場所に書く必要がある等、若干手間な感じではあります。
今後正式にTypeScriptをサポートするDocツールが増えてくると良いですね。
その他
その他罠にハマった&はまりそうなポイント等です
StormのFileWatcherの挙動
「immediate file synchronization」フラグは外しましょう
エディタの編集を細かく追ってコンパイルされるのは良いのですが、編集中のファイルに対してエラーが出力されるので、多少鬱陶しいです。
そもそも重い
1ファイル変更すると、依存関係を追って?他のファイルもコンパイルし直す挙動のようです。
気になる場合はFileWatcherを切って、grunt等で変更されたファイルのみをコンパイルするのが良いかもしれません。
functionキーワードとアローシンタックスによる挙動の違い
クラス内での関数定義時にアローシンタックスを使うとコンパイル時にthisキーワードを巻き上げてくれます。
対してfunctionキーワードによる定義にはこの挙動はありません。
TypeScript
1 2 3 |
[sourcecode lang="java"]var sample = () => { console.log(this); };[/sourcecode] |
JavaScript
1 2 3 4 |
[sourcecode lang="javascript"]var _this = this; var sample = function () { console.log(_this); };[/sourcecode] |
上記により、クラスのプロパティとして関数を定義し、アローシンタックスを使用すると、メソッド自体をコールバックに渡すようなケースでもthisをそのまま利用可能です。
moduleの巻き上げ
コンパイル時に、同一モジュールツリー内のクラス等は「親モジュール名.クラス名」というオブジェクトに置き換えて処理されます。
この際、親モジュール名を変数名(オブジェクト名)として使用していると、処理上不都合なケースがあります。
下記はhlk.commandモジュール内でSequenceクラスのメソッドの引数として親モジュール名同等の名前「command」を使用し、処理中にcommandモジュールにぶら下がるクラス名を参照しているというケースです。
TypeScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
[sourcecode lang="java"]/** * @method add * @param {hlk.command.Abstract} command * @return {hlk.command.Abstract} */ public add(command:Abstract):Abstract { if (command instanceof Abstract) { this._queue.push(cmd); } return this; }[/sourcecode] |
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 |
[sourcecode lang="javascript"]/** * @method add * @param {hlk.command.Abstract} command * @return {hlk.command.Abstract} */ Sequence.prototype.add = function (command) { if (command instanceof command.Abstract) { this._queue.push(cmd); } return this; };[/sourcecode] |
正しい引数が渡されても処理が通りませんね。
この様に、この挙動による罠は割と気付きづらいので注意が必要です。
まとめ
JavaScriptをTypeScriptで書くメリットは充分にあり、デメリットも割と環境で補える部分はあります。
規模の大きなコードでは、現状でもTypeScriptに限らずAltJSで書く方が無難かもしれません。
ただ、思わぬところに落とし穴があったり、まだ言語的に不十分なところもあるので、それを補う手段は気にする必要があるかなという感想です。
注意点等まとめ
- 言語的に不十分なところは規約やDocで補う
- 言語の機能で補える部分は積極的に頼る
- コメントと命名規則は重要
- コンパイル後のコードも見る
- 今後の言語仕様更新のよる影響の考慮
- IDE周りは今後に期待