tl;dr
これで A
を継承したクラス B
で上書きした static
なプロパティを A
の中で参照できる
class A {
static readonly foo = 'Foo!';
get static(): typeof A {
return this.constructor as typeof A;
}
hello() {
console.log(this.static.foo);
}
}
class B {
static readonly foo = 'Bar!';
}
A.hello(); // Foo!
B.hello(); // Bar!
「継承先で、やることは一緒のことをわざわざ書き直したくない」
今回とりあげるモチベーションになっているのはこれ。
例えば『温度』を表現する Kelvin
Celsius
Fahrenheit
が有ったとして
kelvin-0.ts
class Kelvin {
static readonly unit: string = 'K';
static readonly absoluteZero = 0;
constructor(public readonly value: number) {
}
toString() {
return `${this.value}${Kelvin.unit}`;
}
}
celsius-0.ts
class Celsius {
static readonly unit: string = '°C';
static readonly absoluteZero = -273.15;
constructor(public readonly value: number) {
}
toString() {
return `${this.value}${Celsius.unit}`;
}
}
fahrenheit-0.ts
class Fahrenheit {
static readonly unit: string = '°F';
static readonly absoluteZero = -459.67;
constructor(public readonly value: number) {
}
toString() {
return `${this.value}${Fahrenheit.unit}`;
}
}
この時点でざっと思いつくのが、こういうアイデア
- これだと
new Celsius(0).value === new Kelvin(0).value
の結果がtrue
になっておかしい- なのでどの温度の実装クラスでも比較用の絶対温度を返す
t.kelvin: number
なプロパティをもたせたい - ……ということは、どの温度でも絶対温度に変換するメソッドが要る
- なのでどの温度の実装クラスでも比較用の絶対温度を返す
- $-300°C$ とか $-20K$ みたいな温度が存在できてしまう
- $\Delta K$ としての『温度差』は許容したいが、「実際にゃその温度は存在できないよ?」って知るためのプロパティがほしい
- なので
t.existable: boolean
みたいなのを各クラスに生やす- 一応 existable は英単語としてはある。
- 定数として各クラスの
static
プロパティに、 $0K$ 時での温度をabsoluteZero
として持っておけばよさそう
- 上に
TemperatureBase
みたいなクラスを作って、そこで実装しておけばコピペのような定義をせんでよくなるのではないか
ただし、その時にこういう問題がある
「継承した先で上書きする
static
フィールドを、継承元のクラスはどう呼べばいいの?」
this.constructor as typeof A
イディオム
こうする
temperature-interface.ts
export interface Temperature {
/** 温度単位。 °C とか °F とか */
readonly unit: string;
/** 素朴な温度の数値表現 */
readonly value: number;
/** ケルビン温度として変換された時の値。内部で `value` を参照すれば良し */
readonly kelvin: number;
/** 絶対零度以上であれば `true` */
readonly existable: boolean
}
export const isNumber = (x: unknown): x is number => typeof x === 'number';
/** ちょっとしたヘルパー関数 */
export const valueOfKelvin = (t: number | Temperature) => {
return isNumber(t) ? t : t.kelvin;
}
temperature-base.ts
export abstract class TemperatureBase implements Temperature {
/** 温度単位。 °C とか °F とか */
protected static readonly unit: string = undefined;
/** 絶対零度におけるその単位での温度数値 */
protected static readonly absoluteZero: number = undefined;
/** その単位での「1度」上がるごとの絶対温度の差の割合 */
protected static readonly degree: number = undefined;
/** その単位での温度に変換する */
static valueByKelvin(t: number | Temperature): number {
return valueOfKelvin(t) / this.degree + this.absoluteZero;
}
/** 今回のポイント。 `typeof TemperatureBase` を返すのがキモ */
protected get static(): typeof TemperatureBase {
return this.constructor as typeof TemperatureBase;
}
readonly value: number;
readonly existable: boolean;
constructor(t: number | Temperature) {
this.value = isNumber(t) ? t : this.static.valueByKelvin(t);
this.existable = this.value >= this.static.absoluteZero;
}
/// これとかはもうここで実装できる
get kelvin(): number {
return (this.value - this.static.absoluteZero) * this.static.degree;
}
get unit(): string {
return this.static.unit;
}
valueOf(): number {
return this.kelvin;
}
toString(): string {
return `${this.value}${this.unit}`;
}
/** 一応小数点や大きい桁数同士の誤差許容した作りで同じ温度か判定する */
equals(t: Temperature, digits: number = 3): boolean {
if (digits) {
return Math.abs(this.kelvin - t.kelvin) < parseFloat(`1e${-digits}`);
}
// たぶん現実的に同値判定せんほうがいいよ
return this.kelvin === t.kelvin;
}
}
ポイント
- 素朴に
static
領域はabstract
には出来ないので、型はつけておきつつ値をundefined
にしている- コンクリートクラスが実装漏れてた時にすぐわかるのでデフォルト実装をしていない
-
TemperatureBase.unit
と呼ばずに、(this.constructor as typeof TemperatureBase).unit
として呼んでいる- メソッドの呼び出し時にこの
this
が呼び出し元のオブジェクトに紐付いているから出来る - なので、このサブクラスが呼び出す時は「自分のクラスフィールド、クラスプロパティ、クラスメソッドから」選ぶようになる
- メソッドの呼び出し時にこの
Enhancement
んで、冒頭のクラスはこう書き直せる
kelvin.ts
export class Kelvin extends TemperatureBase {
static readonly unit = 'K';
static readonly degree = 1;
static readonly absoluteZero = 0;
}
celsius.ts
export class Celsius extends TemperatureBase {
static readonly unit = '°C';
static readonly degree = 1;
static readonly absoluteZero = -273.15;
}
fahrenheit.ts
export class Fahrenheit extends TemperatureBase {
static readonly unit = '°F';
static readonly degree = 5 / 9;
static readonly absoluteZero = -459.67;
}
実装において、「その温度単位を表現するのに必要なことだけ」記述する程度に済んでいる
- コンストラクタの引数に渡せばそのまま変換される
-
${temp}
で埋め込みゃ単位付きで文字列化される -
a.equals(b)
で単位が違う温度同士でも絶対温度で比較して同じかどうかわかる-
==
===
はオーバーライド出来ないからしょうがないね - ちなみに
+a == +b
とかすればそれぞれが.valueOf()
を呼んでnumber
に変換されるから出来る - とはいえ浮動小数点数の誤差の問題を引き継ぐから、
a.equals(b, 2)
とかで小数点下二桁の誤差無視とかできるように - 大小関係であれば
a > b
とかでOKになる
-
- マイナーなランキン度とかニュートン度とかを後で実装できる。
- けれど、これまで実装した他の温度のクラスに変更を加える必要がなく独立している
全て相互変換できるようになってる。
const t = new Fahrenheit(212);
console.log(t); // "212°F"
console.log(new Celsius(t)); // "100°C"
console.log(new Kelvin(t)); // "373.15K"
const x = new Celsius(0);
console.log(x); // "0°C"
console.log(new Kelvin(x)); // 273.15K
console.log(new Fahrenheit(x)); // 32°F
単位が違う温度同士で大小比較も同値比較もできる
const a = new Celsius(35);
const b = new Celsius(32);
// '35°C > 32°F ? : false'
console.log(`${a} > ${b} ? : ${a > b}`);
// 'because 35°C is 95°F, 32°F is 0°C'
console.log(`because ${a} is ${new Fahrenheit(a)}, ${b} is ${new Celsius(b)}`);
足し算引き算
これだけ改良の余地あり
const a = new Celsius(25);
const b = new Celsius(5);
const c = new Celsius(new Kelvin(+a + +b); // +a + +b で絶対温度の数値型として変換されているので
まぁ改良するとして、生のケルビンを示す数値型を受け取ってインスタンスを作る static fromKelvinValue(kelvin: number)
みたいなのを生やせば良い