ES6のサブクラス化仕様
ES6(ES2015)ではビルトイン・オブジェクトであるNumber
のサブクラスを作る事ができる。
class MyNumber extends Number{
//...
}
ただし、これをやるためにはランタイム側がサブクラス化に対応している必要があり(Babelなどでは対応できない)、現時点(2015-09-08)では対応しているものも多くない。
数少ない対応環境であるChrome (バージョン 45.0.2454.85 (64-bit))をつかって今回はInt
を作ってみる。
Intクラスの作成
コードはシンプルだ。
"use strict";
class Int extends Number{
constructor(v){
super(v|0);
}
}
var a = new Int(12.34);
console.log(a); // 12
console.log(a+a); // 24
Chrome/Opera/io.jsのV8は"use strict";
ディレクティブをつけることでNumber
を継承したサブクラスを作ることができる。
コンストラクタの引数の数値を|0
することで 32bit signed int を作る事が出来る(asm.jsと同じ)。
+
などの算術演算子もNumber
扱いで計算してくれるのでTypeErrorなどにはならない。
同様にUInt
クラスなども作れる。
"use strict";
class UInt extends Number{
constructor(v){
super(v>>>0);
}
}
var u = new UInt(-12.3);
console.log(`u:${u}`); //u:4294967284
問題
無事にInt
クラスができたが、Int
クラスのインスタンスに普通に数値を代入すると普通の数値になってしまう(Number
のインスタンス扱いにすらならない)。
var a = new Int(12.34); //12
console.log(a instanceof Int); //true
console.log(a instanceof Number);//true
a = 56.7;
console.log(a); //56.7
console.log(a instanceof Int); //false
console.log(a instanceof Number);//false
常に整数値になるように、みたいな、型のある言語のようにはいかない。
またNumber
型はnew
なしの使い方ができるが、staticなコンストラクタ、みたいな定義がES6ではできないので以下のような使い方は(親クラスとはことなり)できない。
var a = Int(12.3); //Uncaught TypeError: Class constructors cannot be invoked without 'new'
x|0
やx>>>0
でいいか、となりそう。x|0
やx>>>0
を直接書かないメリットは、一種のマジックナンバーの防止、ぐらいだろうか。
TypeScript
型チェックが欲しいならTypeScript、ということでやってみる。ちょうど出たばかりの1.6betaがビルトインオブジェクトのサブクラス化でエラーが出なくなったのでトライ。-m "es6"
で直接ES6を出力させる。
"use strict";
class Int extends Number{
constructor(v:any){
super(v|0);
}
}
クラス定義は問題ない。ところがもう少し使おうとすると問題がでる。
var a = new Int(12.34);
console.log(a); // 12
console.log(a+a); // error TS2365: Operator '+' cannot be applied to types 'Int' and 'Int'.
+
などの算術演算子がNumber
のサブクラスの演算に対応していない。
そこでvalueOf()
メソッドを定義してみる。valueOf()
メソッドは+
の時に暗黙的に呼ばれるので、戻り値の型としてnumber
型を定義してみてはどうだろう。
"use strict";
class Int extends Number{
constructor(v:any){
super(v|0);
}
valueOf():number{
return super.valueOf();
}
}
var a = new Int(12.34);
console.log(a+a); // error TS2365: Operator '+' cannot be applied to types 'Int' and 'Int'.
うーむ、valueOf()
の戻り値の型までは見てくれないらしい。現状では演算子の両辺で以下のようにするしかない。
console.log(<number>a+<number>a)
TypeScriptに、型チェックのためだけの演算子オーバーロードが欲しい。
なお、以下の問題はTypeScriptでも起きる。
var a = new Int(12.34); //12
console.log(a instanceof Int); //true
console.log(a instanceof Number);//true
a = 56.7;
console.log(a); //56.7
console.log(a instanceof Int); //false
console.log(a instanceof Number);//false
Int
がNumber
を継承しているので代入してもエラーにならない。
ガチガチにやるなら
class IntX{
private _value:number;
constructor(v:any){
this._value = v|0;
}
valueOf():number{
return this._value|0;
}
toString():string{
return this._value.toString();
}
}
var a = new IntX(12.3);
console.log(a); //12
var b = Number(a)+Number(a);
console.log(b); //24
a = 34.5; //error TS2322: Type 'number' is not assignable to type 'IntX'. Property '_value' is missing in type 'Number'.
a = new IntX(1); //ok
ガチガチにやるなら、Number
を継承しないクラスを作る手もある。算術演算子の時のみNumber()
でキャストする必要があるが、インスタンス定義後に勝手に数値を代入できない(整数であっても、だが)。valueOf()
やtoString()
を定義しておくことで数値的な扱いもできる。
Proposal #4639
なお、ちょうど今現在面白い提案がされている。これが実装されると以下のようなことができるかもしれない。
//ts
let a = int<32>(x);
//js
var a = x | 0;
//ts
int<8>(x);
//js
(x | 0) << 24 >> 24
まとめ
- ES6はビルトインオブジェクトを継承できる
- でも出来る環境は限られる
- Intっぽいものも作る事ができる
- けど、型のあるような言語っぽくはならない
- TypeScriptでも同様にできる
- 1.6以降
- 厳格に型チェックしたいならNumberを継承しない方がいいかも