LoginSignup
7
5

More than 3 years have passed since last update.

Typescript で上書きした static プロパティを元のクラスから使うようにする

Last updated at Posted at 2020-09-07

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) みたいなのを生やせば良い

7
5
2

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
7
5