9
9

More than 5 years have passed since last update.

TypeScript/ES6+ES7でクラスメンバーの追加を阻止

Last updated at Posted at 2015-09-16

ガチガチに固めた、静的な言語のようなクラスをJavaScript(TypeScript+ES6+ES7)で作ってみるシリーズ。

クラスメンバーの追加を阻止する

JavaやC#などのクラスは基本的には定義後にメンバープロパティを追加できない。TypeScriptも同様だが、あくまでもコンパイル時チェックであって、実行時は普通のjsなので追加できてしまう。

ES5の機能にObject.preventExtensions(),Object.seal(),Object.freeze()があり、これらを使うとクラスメンバープロパティの追加を阻止できる1

プロパティを後から追加できないクラス
"use strict";
class A{
    constructor(){
        Object.preventExtensions(this);
    }
    prop = 1;
}

//実行
var a = new A();
a.prop2 = 100;      //error!:Uncaught TypeError: Can't add property prop2, object is not extensible

TypeScript PlayGround | Babel

上記のコードで2はコンストラクタでObject.preventExtensions()の対象にクラスインスタンス(this)を指定する事ですべてのクラスインスタンスへのプロパティ追加を阻止している。

なお、この方法ではTypeScriptのコンパイル時チェックをすり抜ける、ブラケットアクセスであっても阻止できる。

ブラケットアクセスでも阻止
a["prop2"] = 100;   //runtime error!

3つのメソッドの違い

  1. プロパティの値を変更・削除してもいい
    • Object.preventExtensions()
    • ただし、プロパティが関数の場合は再割当禁止
  2. プロパティの値を変更してもいいが削除はダメ
    • Object.seal()
  3. プロパティの値の変更もダメ
    • Object.freeze()
クラス
//base
class A{
    prop = 1;
    constructor(){
        /* Object.*(this); */
    }
    func(){}
}
var a = new A();
3パターンの違い
//Object.preventExtensions()
a.prop = 1000;  //OK
delete a.prop;  //OK
a.func = () => {};  //NG
a.prop = () => {};  //OK (TSではNG)

//Object.seal()
a.prop = 1000;  //OK
delete a.prop;  //NG
a.func = () => {};  //NG
a.prop = () => {};  //OK (TSではNG)

//Object.freeze()
a.prop = 1000;  //NG
delete a.prop;  //NG
a.func = () => {};  //NG
a.prop = () => {};  //NG

JavaやC#などのクラスにはpreventExtensions()seal()を指定した時がイメージが近い。freeze()は全てがreadonlyなので、Javaでいえば全部のメンバー変数にfinal/C#で言えば全部のメンバー変数にreadonlyを指定したのと近い。

デコレータでアクセス修飾子風に指定する

上記で無事実現できたが、1行とは言え毎回コンストラクタ内で指定するのは他のコードに混じって意味がわかりにくい。デコレータをつかうことで、アクセス修飾子風に記述することができる。

デコレータでアクセス修飾子
@seal class A{
    constructor(){}
    prop = 1;
}

デコレータ自体の定義は以下。コンストラクタを置き換えるためやや長いが一度定義してしまえばあとは簡単。

@sealデコレータの定義
function seal<T extends Function>(t:T):T|void{
    let base = t;

    function construct(constructor:T, args:any[]) {
        let c: any = function () {
            return constructor.apply(this, args);
        };
        c.prototype = constructor.prototype;
        return new c();
    }

    let func: any = (...args:any[])=>{
        let a = construct(base, args);
        Object.seal(a); //ここで指定
        return a;
    };

    func.prototype = base.prototype;

    return func;
}
実行時
var a = new A();
a.prop = 10;
a.prop2 = 10;   //error!

上記コードはES7のデコレータを使うので、TypeScript1.5.3以降、--experimentalDecoratorsオプションを有効にすることで使える。型指定を省けば、Babelでも有効

※なお、@sealedとしなかったのはC#のsealedとかぶるため。C#のsealedは継承不可の指定のため意味合いが異なる。

注意点

  • 入れ子のプロパティ、つまりプロパティの子プロパティには適応されない
    • 入れ子まで指定したい場合は、子や孫までずっと走査して指定する必要がある
  • デコレータの場合は、継承したクラスにも同じくデコレータを指定する必要がある
    • コンストラクタ内で指定の場合は継承するだけでOK
  • ES6の範囲内だけでやる場合、コンストラクタ内で変数プロパティを宣言後にObject.seal(this)とする必要がある
    • 後からやらないと一切追加できなくなるため

  1. なのでES3環境では実現できない。 

  2. このコードはTSとexperimentalフラグをONにしたBebelで有効。 

9
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
9