TypeScript でビットフラグを取り回す

2023/12/19に公開

ビットフラグとは

ビットフラグは整数型の各ビットをフラグに見立てて値を保持するテクニックです。ビットフラグを使うと関連するフラグを効率的に管理することができます。

例えばゲームソフトの配信プラットフォームを表現する場合を考えてみましょう。

type Platform = "PS5" | "Xbox" | "Switch" | "STEAM";

ゲームソフトは複数のプラットフォームで配信される場合があります。各プラットフォームの配信状況をそれぞれ Boolean で管理してもよいですが、ビットフラグを活用すると単一の Int で配信状況を表現できます。PS5でのみ配信しているゲームは 00011、 Xbox と STEAM で配信している場合は 101010 となります。

Python だと標準ライブラリの enum の中に IntFlag というクラスの形でビットフラグが提供されています。

これを TypeScript で実現する方法を考えてみましょう。

DB上で Int として表現されているフラグを TypeScript の世界でいい感じに使いたいといったユースケースを想定しています。

どんなインターフェースにするか

フラグ管理をするにあたっては以下の機能が必要そうです。

  • 特定のフラグが立っているかどうかの真偽値を返すメソッド
  • 特定のフラグを ON/OFF できるメソッド
  • 立っているフラグの集合を受け取るメソッド

Python にならって IntFlag クラスを実装する手もありますが...こういったプリミティブなクラスはアプリケーションの至る所で使われる可能性があるので新しいクラスの導入には慎重なりたいところです。

要件をよく眺めてみるとこれらの機能はまさしく Set が提供してくれていることに気がつきます。フラグを新しいクラスとして実装するのではなく、数値を型付きの Union 型の Set へ変換するアプローチを採用してみました。関数 bitsToSet と逆の変換を行う setToBits を定義します。

const Platforms = ["PS5", "Xbox", "Switch", "STEAM"] as const;
type Platform = (typeof Platforms)[number];

const platforms = bitsToSet(Platforms)(10);
// => Set (2) {"Xbox", "STEAM"}

platforms.add("PS5");
// => Set (3) {"Xbox", "STEAM", "PS5"} 

setToBits(Platforms)(platforms);
// => 11

変換後の集合の型は Set<"PS5" | "Xbox" | "Switch" | "STEAM"> となるので、存在しないフラグを立てようとすると型エラーになります。

// 型エラー
platforms.add("iOS");

実装

実装はこれだけです。

function bitsToSet<T extends string>(
  flags: readonly T[]
): (bits: number) => Set<T[][number]> {
  return (bits: number) => new Set(flags.filter((_, i) => bits & (1 << i)));
}

function setToBits<T extends string>(
  flags: readonly T[]
): (set: Set<T[][number]>) => number {
  return (set: Set<T[][number]>) =>
    flags.reduce((b, v, i) => (set.has(v) ? b | (1 << i) : b), 0);
}

このアプローチの欠点

「Set に変換する」というアプローチを取ったおかげで短いコードで要件を実現できました。一方で AND, OR, XOR などのビット演算を活用した集合操作が行えなくなってしまいました。Set のメソッドがもっと増える[1]といいのですが...。

実現したい要件次第では別の実装を検討する必要があります。

脚注
  1. https://github.com/tc39/proposal-set-methods ↩︎

Discussion