クラスしたい(class)

 1class SimpleFrame {
 2  private readonly data: Record<string, unknown[]>;
 3
 4  private constructor(data: Record<string, unknown[]>) {
 5    this.data = data;
 6  }
 7
 8  static fromColumns(columns: Record<string, unknown[]>): SimpleFrame {
 9    return new SimpleFrame(columns);
10  }
11
12  get headers(): string[] {
13    return Object.keys(this.data);
14  }
15}
16
17const frame = SimpleFrame.fromColumns({ name: ["Alice", "Bob"], age: [25, 30] });
18frame.headers;  // -> ["name", "age"]

classでクラスを定義できます。 上のサンプルは、列指向のデータ(列名 → 値の配列)を保持するだけの最小限のクラスです。

constructorは、クラスのインスタンスを初期化するときに呼ばれる関数です。 this.data = data;のように、渡された値をインスタンスのプロパティに保存するのが典型的な使い方です。

プロパティを非公開にしたい(private)

1class SimpleFrame {
2  private readonly data: Record<string, unknown[]>;
3  // ...
4}

privateを付けたプロパティは、クラスの外から直接アクセスできません。 readonlyを組み合わせると、コンストラクター以外での再代入も禁止できます。 内部で保持しているデータを外から書き換えられたくない場合に使います。

ヒント

TypeScriptのprivateはコンパイル時のチェックであり、実行時には強制されません。 本当に実行時にもアクセスを禁止したい場合は、 #dataのようなJavaScript標準のプライベートフィールドを使います。

ファクトリーメソッドしたい(static)

 1class SimpleFrame {
 2  private readonly data: Record<string, unknown[]>;
 3
 4  private constructor(data: Record<string, unknown[]>) {
 5    this.data = data;
 6  }
 7
 8  static fromColumns(columns: Record<string, unknown[]>): SimpleFrame {
 9    return new SimpleFrame(columns);
10  }
11}
12
13const frame = SimpleFrame.fromColumns({ name: ["Alice"] });  // OK
14const frame2 = new SimpleFrame({ name: ["Alice"] });         // エラー:コンストラクターがprivate

constructorにprivate(もしくはprotected)を付けると、 クラスの外からnew クラス名(...)で直接インスタンス化できなくなります。 代わりにstaticなファクトリーメソッド(ここではfromColumns)を用意して、 そこから間接的にインスタンスを作らせます。

入力の形式ごとにfromArrays・fromRows・fromColumnsのような複数のファクトリーメソッドを用意すると、 それぞれの名前で「何を渡せばよいか」が分かりやすくなります。

メソッドを追加したい

 1class SimpleFrame {
 2  private readonly data: Record<string, unknown[]>;
 3
 4  private constructor(data: Record<string, unknown[]>) {
 5    this.data = data;
 6  }
 7
 8  static fromColumns(columns: Record<string, unknown[]>): SimpleFrame {
 9    return new SimpleFrame(columns);
10  }
11
12  select(headers: string[]): SimpleFrame {
13    const result: Record<string, unknown[]> = {};
14    for (const h of headers) {
15      if (!(h in this.data)) {
16        throw new Error(`SimpleFrame.select: column "${h}" does not exist`);
17      }
18      result[h] = this.data[h];
19    }
20    return new SimpleFrame(result);
21  }
22}
23
24const frame = SimpleFrame.fromColumns({ name: ["Alice", "Bob"], age: [25, 30] });
25const names = frame.select(["name"]);   // 指定した列だけを持つ新しいSimpleFrame
26frame.select(["missing"]);              // -> Error: column "missing" does not exist

インスタンスメソッド(select)の中では、thisでインスタンス自身のプロパティにアクセスできます。 既存のインスタンスを直接書き換えるのではなく、 selectの結果として新しいインスタンスを返すようにしておくと、 元のデータを保持したまま加工を連鎖させやすくなります(イミュータブルな設計)。

存在しない列名を指定したときは、throwでエラーを投げて早めに気づけるようにしています。

getterを使いたい(get)

1class SimpleFrame {
2  // ...
3  get headers(): string[] {
4    return Object.keys(this.data);
5  }
6}
7
8const frame = SimpleFrame.fromColumns({ name: ["Alice"], age: [25] });
9frame.headers;  // -> ["name", "age"](メソッドだが () は不要)

getを付けたメソッドは、プロパティのように()なしでアクセスできます。 「保持しているデータから毎回計算して返す値」を、 あたかも普通のプロパティであるかのように見せたいときに使います。

prototypeを使った方法

もともとJavaScriptには「クラス」を作る機能がなく、 prototypeという機能を使って「クラスのような」実装をしていました。 これから新しく作成するスクリプトでは、この書き方を真似する必要はありません。

 1function SimpleFrame(data) {
 2    this.data = data;
 3}
 4
 5SimpleFrame.fromColumns = function (columns) {
 6    return new SimpleFrame(columns);
 7};
 8
 9SimpleFrame.prototype.select = function (headers) {
10    var result = {};
11    for (var i = 0; i < headers.length; i++) {
12        var h = headers[i];
13        if (!(h in this.data)) {
14            throw new Error('SimpleFrame.select: column "' + h + '" does not exist');
15        }
16        result[h] = this.data[h];
17    }
18    return new SimpleFrame(result);
19};

function自体がコンストラクターの役割を持ち、 関数名.prototype.メソッド名 = function() {...}の形でメソッドを追加します。 staticに相当するものは、関数オブジェクト自体にプロパティを生やして表現します(SimpleFrame.fromColumns = ...)。 privateのようなアクセス制御の仕組みはないため、this.dataは常に外部から書き換え可能です。

リファレンス