読者です 読者をやめる 読者になる 読者になる

文字列で指定する既知のパラメータをTypeScriptで型付けする

TypeScript

1. Enumに置き換える

JavaScriptでは、ライブラリ等に渡すパラメータを文字列で指定することが多いのですが、指定可能な値が分からなかったり、タイプミスによるバグを作り込む可能性があったりで嫌ですよね。

例えば以下のような場合、lineCap には"butt" / "round" / "square" が指定可能なのですが、ドキュメントを調べないと分かりませんし、 Typoしても気付き難いです。

//Canvasに描画する線の終端部分の形状を指定する
var rc = canvas.getContext("2d");
rc.lineCap = "round"; //"butt" or "round" or "square"

このような場合、TypeScriptにあるEnum型が使えそうです。

//設定可能な値をEnumで定義
enum LineCap { butt, round, square }

var rc = canvas.getContext("2d");
//enumの要素名を文字列で取得
rc.lineCap = LineCap[LineCap.round]; 

enumの要素名(butt, round, square)を取得するには、LineCap[lineCap.round]の様に記述する必要があるのですね。
C#enum に慣れていると、LineCap.round.toString();とやりたくなるのですが、LineCap.roundは単なる数値(1)なので、この場合"1"が設定されてしまいます。)

あんまり嬉しくない?

上記サンプルでは、記述量が多くなるだけであまり有難味が無いように思えます。
rc.lineCap = LineCap[LineCap.round]のような記述も不細工です。

実際に使う際には、enum ⇔ 文字列パラメータ の変換部分をラッパークラス等で隠ぺいして、enum の受け渡しだけで済むようにした方が良いでしょう。

例として、Canvasに描画するストロークのスタイルを指定するためのクラス(Penクラス)を作ってみます。

enum LineCap { butt, round, square }
enum LineJoin { bevel, round, miter }

class Pen {
    constructor(public color: Color, 
        public width: number= 1,
        public lineDash: number[]= [],
        public lineCap: LineCap= LineCap.butt,
        public lineJoin = LineJoin.bevel,
        public miterLimit: number = 10.0) { }

    //ストロークのスタイルをまとめて設定する。
    public applyTo(rc: CanvasRenderingContext2D) {
        rc.strokeStyle = this.color.cssColor;
        if (rc.setLineDash != undefined) {
            rc.setLineDash(this.lineDash);
        }
        rc.lineWidth = this.width
        rc.lineCap = LineCap[this.lineCap];
        rc.lineJoin = LineJoin[this.lineJoin];
        rc.miterLimit = this.miterLimit;
    }
}

このクラスを使う側では、コンストラクタで各種パラメータを指定するのですが、
その際、(IDEを使用していることが前提ですが、)LineCap および LineJoin では入力補完が利き、一覧から選択するだけでOKとなります。

var rc = canvas.getContext("2d");
//IDEを使用していれば、`LineCap.`や`LineJoin.` と入力して、入力候補から選ぶだけ!
var pen = new Pen(Color.Red, 1, [], LineCap.Round, LineJoin.Round);
pen.applyTo(rc);

TypeScriptのenum について

enumJavaScriptコンパイルすると、以下のような連想配列に展開されるようです。

var LineCap;
(function (LineCap) {
    LineCap[LineCap["butt"] = 0] = "butt";
    LineCap[LineCap["round"] = 1] = "round";
    LineCap[LineCap["square"] = 2] = "square";
})(LineCap || (LineCap = {}));

/*
LineCap[0] = "butt";
LineCap[1] = "round";
LineCap[2] = "square";
LineCap["butt"] = 0;
LineCap["round"] = 1;
LineCap["square"] = 2;
*/

//LineCap.round は、コンパイルするとNumberのリテラルに置き換えられる。
//var a = LineCap.round;
var a = 1 /* round */;
//var b = LineCap[LineCap.round];
var b = LineCap[1 /*round*/];

シンボルとしては使えない文字列を使用したい場合

引用符で囲めば、enumの要素名として使用できるようですが、インデクサからでしかアクセスできなくなってしまう為、残念ながら今回のような用途には使えそうにありません。

//これはNGだけど
enum Test{
   1ab = 0,     //数字から始まる
   abc def = 1, //スペースを含む
}

//これならOK、コンパイルが通る
enum Test{
   "1ab" = 0,
   "abc def" = 1,
}

但し、アクセスはインデクサからのみとなります。入力候補にも出てきません。

Test.1ab     //コンパイルエラー
Test."1ab"   //コンパイルエラー
Test["1ab"]  //OK

2. Static フィールドに置き換える

以下のように、Staticなフィールドを持つクラスに置き換える方法でも良いかもしれません。 これであれば、置き換えたいパラメータがどんな文字列でも(シンボルとして使えなくても)関係なく使えます。

class LineCap {
    constructor(private _index:number, private _value:string){}
    public get index():number { return this._index;}
    public get value():string { return this._value}

    static Butt:LineCap = new LineCap(0,"butt");
    static Round:LineCap = new LineCap(1,"round");
    static Round:LineCap = new LineCap(2,"square");
}

var lineCapIndex = LineCap.Round.index;   //1
var lineCapString = LineCap.Round.value;  //"round"

以下のような基底クラスを用意しておけば、定義が楽になりますし、文字列以外の型も使えて便利です。

class EnumBase<TValue> {
    constructor(private _index:number, private _value:TValue){}
    public get index():number { return this._index;}
    public get value():TVale{ return this._value}
}

class LineCap extends EnumBase<string>{
    static Butt:LineCap = new LineCap(0,"butt");
    static Round:LineCap = new LineCap(1,"round");
    static Round:LineCap = new LineCap(2,"square");
}

最後に

今回のような用途で、enumを使うのはちょっと無理があるような気がしてきました。

以下の様にEnumの値を文字列で指定出来たらいいのに

enum Test{
    None = "",
    Hoge = "hoge",
    Piyo = "piyo piyo"
}