JavascriptやTypescriptで登場するSymbol()って何だろう?と思って調べた話

JS,TS系のGithubリポジトリを眺めていてSymbol()という記述を見かけました。 ライブラリのコアな部分で見かけたことはあるのですが、いまいち使い方が分かっていなかったので調べてみました。

まずは公式を見る

まずは説明を読んでみましょう。

データ型 symbol は、プリミティブデータ型です。Symbol() 関数は、symbol 型の値を返します。これは組み込みオブジェクトを公開するための静的プロパティを持ち、グローバルシンボルレジストリを公開するための静的メソッドを持つので、組み込みオブジェクトクラスのようにも見えますが、コンストラクターとしての機能を持たず、"new Symbol()" はサポートされていません。

うーん、分かったようなそうでもないような。

こんな記述もあります。

Symbol() から返されるすべてのシンボル値は一意です。シンボル値は、オブジェクトプロパティの識別子として使用できます。

ポイントはこの記述で、要は任意のオブジェクトのプロパティを作れるということです。

const HOGE = Symbol('hoge');

const obj = {
  [HOGE]: 'hogehoge'
};

通常のプロパティと何が違う?

上記のコードだけを見ると「別にhogeというプロパティを作ればいいじゃん」と思うかもしれません。

// これと何が違う?
const obj = {
  hoge: 'hogehoge'
};

通常のプロパティだと困るケース

例えば以下のようなレスポンスのAPIを想定します。 記事一覧を返してくれるようなAPIです。

const articleList = [
    {no: 1001, title: "hoge"},
    {no: 3004, title: "fuga"},
    {no: 5005, title: "puni"}
]

noはAPIの提供元で割り振られたものですが、自作アプリケーションのためにidというプロパティを割り振ることを考えます。

// articleに任意のidの値を追加して返却する
const addId = (article) => {
    return {
        ...article,
        id : Math.floor( Math.random() * 100000 )
    }
}

// APIから取得してきたデータにidを追加する
const processedArticleList = articleList.map(article => addId(article));

当面はこれで問題なく動作していました。 しかしながら後になってAPIの仕様が変更となり、APIのレスポンスで「今までnoだったプロパティ名がidに変更になった」場合はどうでしょうか?

// API仕様変更に伴いnoがidになった
const articleList = [
    {id: 1001, title: "hoge"},
    {id: 3004, title: "fuga"},
    {id: 5005, title: "puni"}
];

// articleに任意のidの値を追加して返却する
const addId = (article) => {
    return {
        ...article,
        id : Math.floor( Math.random() * 100000 )
    }
}

// APIから取得してきたデータにidを追加する
const processedArticleList = articleList.map(article => addId(article));

この場合、元々レスポンスに含まれていたidのプロパティをaddIdが上書きしてしまうことになります。 慌ててaddIdで付与するプロパティ名をcustomIdなどに変えましたが、影響範囲が膨大になってしまったり・・・

Symbolは他と重複しないプロパティである

こういったことをSymbol()なら防ぐことができます。 Syombol()は平たく説明すると「アプリケーション独自の重複しない定数プロパティ」です。

const id = Symbol('id');

// 共存できる
const obj = {
  [id]: 'hogehoge',
  id: 'fugafuga'
}

console.log(obj[id]); // --> hogehoge
console.log(obj.id); // --> fugafuga

こうすることで、先の例のように「意図しないプロパティの重複」を防ぐことができます。 細かい特徴として、以下のようにJSON化した際にSymbolを使ったプロパティの値は含まれない点があります。

console.log(JSON.stringify(obj)); // --> "{"id":"fugafuga"}" 

このことから、Symbolは基本的には「アプリケーション内で使用する、重複しないプロパティ」のような用途として使っていくことになります。

知らず知らずのうちにSymbolを使っている例

実は知らないうちにSymbolを使っていることがあります。 Symbolにはいくつか静的なプロパティが用意されています。 例えばSymbol.iteratorなどです。

console.log(Symbol.iterator); // --> Symbol(Symbol.iterator) 

これはイテラブルな値を使う際に使用できるプロパティで、みなさんご存知のfor-of構文で内部的に使用されています。 イテラブルな値については、自分が過去に書いた以下の記事が参考になります。

参考:Zenn - TS】今さら聞けないイテレータ・ジェネレータ

まとめ

今回はSymbolについて紹介しました。 アプリケーションのコアな部分や、自作のライブラリを作る場合などに用いることがほとんどなため、普段はあまり使用しないものかもしれません。

しかしながら、具体例で挙げたケースのように役立つ場面はあると思うので、一度手元でコードを動かしてみて「こんな感じなんだ」と触れておくといいのかと思います。

今回の内容が役立ちましたら幸いです。

SNSでシェアする