3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

`Symbol.toPrimitiveを使って、JavaScriptのObjectをプリミティブ型として扱えるようにする

Posted at

ハイサイ!オースティンやいびーん。

概要

ES6クラスで定義したオブジェクトを自動的にプリミティブ型に変換する方法を紹介します。

要するに、オブジェクトなのに、掛け算、割り算など、文字列の連結などが簡単にできるということです。

なぜ

最近、筆者はAngular 16のSignalについていくつか記事を書いていますが、Signalをテンプレート、もしくはcomputedのコールバックで使う時に、そのSignalを呼ぶ「()」必要があることに違和感を感じています。

数値を保持しているSignalなら、this.count * this.multiplierみたいに扱えたらいいのになあという欲望が湧いたのです。

それも、できることを知っているから欲しいと思うのです。

原始的なSignalを実装する

まず、原始的なSignalを実装しましょう。

Signalは二つの要件を満たす必要があります:

  1. 自身の値を保持する
  2. 保持している値が変わった時にイベントを配信する(通知機能)

上記の二つの要件を満たした実装は以下の通りになります。

//@ts-check

/**
 * @template T
 */
class Signal {
  /** @type {T} */
  #value;
  /** @type {Set<(v: T) => void>} */
  #subscriptions = new Set();

  /**
   * @param initValue {T}
   */
  constructor(initValue) {
    this.#value = initValue;
  }

  /** @param value {T} */
  set(value) {
    this.#value = value;
    this.next();
  }

  /** @returns {T} */
  get value() {
    return this.#value
  }

  /**
   * 購読者のコールバック関数を呼ぶ
   * @private
   */
  next() {
    this.#subscriptions.forEach((sub) => sub(this.#value));
  }

  /**
   * @param nextCallback {(V: T) => void}
   * 購読を取りやめるコールバック(unsubscribe)を返す
   * @returns () => boolean
   */
  subscribe(nextCallback) {
    this.#subscriptions.add(nextCallback);
    return () => this.#subscriptions.delete(nextCallback);
  }
}

上記の実装を以下のように試すことができます。ブラウザのコンソールでコピペーしてもOKです。

const count = new Signal(1);

count.subscribe((val) => console.log('Count changed: ', val));

count.set(2); // Count changed:  2

プリマティブとして動いてくれるように修正する

我々の素晴らしい英雄なるSignalを再発明したので、こいつがJavaScriptの演算と仲良くできるようにしましょう。

これを実現するためにはSymbol.toPrimitiveを使って、Signalクラスにメソッドを定義します。

//@ts-check

/**
 * @template T
 */
class Signal {

...

  /**
   * @param hint {string}
   * @returns {any}
   */
  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case "string":
        return String(this.#value);
      case "number":
        return Number(this.#value);
      default:
        return this.#value;
    }
  }
}

非常に簡単なのですが、強力的な結果になります。何が起きているのかを説明する前に、まず試してみましょう。

数字の演算だと以下のようにできます。

const count = new Signal(2);
const multiplier = new Signal(2);

console.log(count * multiplier); // 4
multiplier.set(3);
console.log(count ** multiplier); // 8
console.log(count - multiplier); // -1
console.log(count / multiplier); // 0.6666...

両方ともプリマティブじゃないのに、JavaScriptのインタープリターは難なく演算ができていますよね。魔法みたいです。

文字列でも同様にできます。

const firstName = new Signal('Austin');
const lastName = new Signal('Inoue');

console.log(lastName + firstName); // InoueAustin
lastName.set('Mayer');
console.log(firstName + lastName); // AustinMayer

何が起きているのか、魔法の[Symbol.toPrimitive]メソッド

魔法に見えますが、JavaScriptのインタープリターが単に仕事をこなしているだけなのです。

上記の演算をやろうとした時に、

  1. インタープリターは数字かどうかをみます
  2. 数字じゃなかったら、数字に変換できないかを調べる

上記の場合は2に入りますが、「数字に変換できるかどうか」を調べる時に、オブジェクトだったらSymbol.toPrimitiveのプロパティがあるかどうかをみています。
あれば、myObject[Symbol.toPrimitive]('number')を呼んで、その結果を持って計算を試みています。

JavaScriptは素晴らしいな、と思うのはこういうところです。Pythonもできたはずです。

実際に、[Symbol.toPrimitive]のメソッドにconsole.logを入れて実行してみましょう。

class Signal {
...

  [Symbol.toPrimitive](hint) {
    console.log('toPrimitive hint:', hint);
    switch (hint) {
      case "string":
        return String(this.#value);
      case "number":
        return Number(this.#value);
      default:
        return this.#value;
    }
  }
}

もう一度演算の計算をやってみると以下のログが出力されます。

const count = new Signal(2);
const multiplier = new Signal(2);

console.log(count * multiplier); // toPrimitive hint: number

どう動いているのか見えてきたので、魔法じゃないことがわかります。

まとめ

JavaScriptのSymbol.toPrimitiveでオブジェクトでも演算などができるように、プリミティブ型として扱えるようにする方法を紹介しました。

筆者は、Angularのsignalのように、computedのコールバックでもsubscribeが渡られるようにする方法を探っていきたいなと思っています。

3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?