TypeScript、ごく最近、初めて触りました。名前にTypeと付いているくらいだから、型システムがウリのひとつなんでしょう。
パッと見ですが、TypeScriptの型システム、なかなか良さそうです。あえて意地悪に、うまくいかなそうなケースをツツいてみました。
例題は、色を表現するRGBデータです。それはこんな(↓)感じ。
class 色のRGBデータ { r: 0から255のあいだの整数; g: 0から255のあいだの整数; b: 0から255のあいだの整数; }
このクラスのインスタンスが、生成されて死ぬ(プログラムが終わるかゴミ集めされる)まで、ずっと妥当性(validity)を保っていて欲しいわけです。妥当性は、上の擬似コードに書かれているとおり「0から255のあいだの整数」であることです。妥当性の確認用の関数を書いておきます。
function assert(cond:boolean, msg:string = "") :void { if (!cond) throw "Assertion Failed: " + msg; } function isInt(x:any) :boolean { return (typeof x === 'number') && (x % 1 === 0); } function validRgbColor(obj:any) :boolean { return ( isInt(obj.r) && isInt(obj.g) && isInt(obj.b) && (0 <= obj.r && obj.r <= 255) && (0 <= obj.g && obj.g <= 255) && (0 <= obj.b && obj.b <= 255) ); }
とりあえず次のようなクラスを書いてみましょう。
class RgbColor1 { r:number; g:number; b:number; constructor(r:number, g:number, b:number) { this.r = r; this.g = g; this.b = b; } }
TypeScriptは契約(Contract)機能をサポートしてないので、手であっちこっちに assert(validRgbColor(theObject)) を埋め込んだとしましょう。Assertion Failed がいくらでも出そうですよね。残念ながら、この例題に関して型システムはほぼ無力です。
なぜ無力なのか、どうやって対処できるかを見ていきます。
JavaScriptにはnumber型はあってもinteger型がありません(JSONスキーマではinteger型があります)。TypeScriptもinteger型を持たないので、「値は整数である」という制約を型システムで保証することができません。
「0から255のあいだの整数」という制約もTypeScript型システムではまったく扱えません。TypeScriptに限らず、部分集合型をサポートしている型システムはあまり見かけませんね。8ビット符号なし整数の型があればいいだろう、と思うかも知れませんが、「0から100のあいだの整数」だともうダメです。
しょうがないので、型チェックと範囲チェックを手で書きます。
class RgbColor2 { r:number; g:number; b:number; constructor(r:number, g:number, b:number) { if (!isInt(r) || !isInt(g) || !isInt(b)) throw "Type Error"; if (!(0 <= r && r <= 255) || !(0 <= g && g <= 255) || !(0 <= b && b <= 255)) throw "Range Error"; this.r = r; this.g = g; this.b = b; } }
これで不正な(妥当ではない)インスタンス生成は避けられます。
しかし、実行中にプロパティ値をいじられたら、容易に不正な状態になります。イミュータブル(変更不可能)にするのが良さそうです。
class RgbColor3 { private _r:number; private _g:number; private _b:number; constructor(r:number, g:number, b:number) { if (!isInt(r) || !isInt(g) || !isInt(b)) throw "Type Error"; if (!(0 <= r && r <= 255) || !(0 <= g && g <= 255) || !(0 <= b && b <= 255)) throw "Range Error"; this._r = r; this._g = g; this._b = b; } get r():number {return this._r} get g():number {return this._g} get b():number {return this._b} }
もしなんかの事情で変更が必要なら …… 変更操作にもチェックを入れるしかいないです。
class RgbColor4 { private _r:number; private _g:number; private _b:number; constructor(r:number, g:number, b:number) { if (!isInt(r) || !isInt(g) || !isInt(b)) throw "Type Error"; if (!(0 <= r && r <= 255) || !(0 <= g && g <= 255) || !(0 <= b && b <= 255)) throw "Range Error"; this._r = r; this._g = g; this._b = b; } get r():number {return this._r} get g():number {return this._g} get b():number {return this._b} set r(val:number) { if (!isInt(val)) throw "Type Error"; if (!(0 <= val && val <= 255)) throw "Range Error"; this._r = val; } set g(val:number) { if (!isInt(val)) throw "Type Error"; if (!(0 <= val && val <= 255)) throw "Range Error"; this._g = val; } set b(val:number) { if (!isInt(val)) throw "Type Error"; if (!(0 <= val && val <= 255)) throw "Range Error"; this._b = val; } }
アーア、なんてこったい。
チェックコードでプログラムが肥大するので嫌気がさしますが、そのチェックは実行時に行いますよね。つまり、実行時になにかマズイことが起きる可能性があるわけです。マズイことが実際に起きて例外が発生した後の対処もどこかに書かなくてはなりません。
エラーメッセージ出して処理を中断したら、そのメッセージを見た人からの問い合わせに答える体制も必要でしょうし、菓子折り持って謝りに行く準備もしたほうがいいかもしれません。
もしコンパイル時に「0から255のあいだの整数」をチェックできたら、問い合わせも来ないし菓子折りも要らなくなります。だって、実行時にマズイことが起きないのですから。静的チェックってありがたいでしょ。
しかしながら、型システムに部分集合型を導入するのはとても大変です。整数型の導入はたいしたことないけど、整数も数(number)全体の部分集合型とみなせないこともないです -- そんなこと言ったら、なんだってanyの部分集合型だ、って話もあるけど。
JSONスキーマでは、スキーマ属性という形で簡単な部分集合型をサポートしています。JSONスキーマベースのCatyスキーマもスキーマ属性を書けます。もし、integer型にスキーマ属性minimum, maximumが使えるなら、次のような感じになります。
type colorStrength = integer(minimum=0, maximum=255); class RgbColor3a { r:colorStrength; g:colorStrength; b:colorStrength; constructor(r:colorStrength, g:colorStrength, b:colorStrength) { this.r = r; this.g = g; this.b = b; } }
以上のようなことはTypeScript固有の話ではないですね。一般的に難しいことをTypeScriptの弱点のように言うと不公平かつ可哀そうなので、話を変えます。
nullやundefinedの扱いはどうかな? と気になったんですが:
class RgbColor5 { r:number; g:number; b:number; constructor(r:number, g:number, b:number) { this.r = null; this.g = undefined; } }
これ、問題なくコンパイルできてしまいます。って、それどうなのよ? r, g, bは数値型と宣言しているのに、コンストラクタがその制約を守れてない。でもおトガメ無し。
number型プロパティにnullやundefinedが代入できているってことは、nullやundefinedを「どの型にも含まれる特殊な値」とみなしているのでしょう。この解釈の前例は多くて主流かも知れませんが、悪習でしょう。やめろよ、もう。
例えば、void型(TypeScriptにあります)を、「nullまたはundefined」と解釈すれば、ユニオン型(TypeScriptにあります)と組合せて次のように書けます。
class Person { firstName: (string|void); lastName: string; // ... }
今とは解釈を変えて、stringは「ほんとに文字列がある!」ことを要求すれば、「firstNameは省略可能で、lastNameが必須」であることを表現できます。オプション型(Maybeモナド)を入れてもいいです。いずれにしても、「stringやnumberのなかに、nullとundefinedが含まれる」ってロクデモナイ伝統は断ち切るべき。
他にも「これはどうなの?」な点は幾つかありますが、重箱の隅をつつきたくなるのは、おおむね良く出来ているからです。