TypeScript: 公称型のクラスを作成する

TypeScript: 公称型のクラスを作成する

October 30, 2022

TypeScript のクラスは構造的部分型 #

例えば TypeScript で次のようなユーザ名を意味するバリューオブジェクトを作成したとします。

class UserName {
  public readonly value: string;

  public constructor(value: string) {
    // ユーザ名は 3 文字以上でなければいけない
    if (value.length < 3) {
      throw new Error();
    }
    this.value = value;
  }
}

printUserName 関数には、次のように引数を渡すことができます。

function printUserName(userName: UserName): void {
  console.log(userName.value);
}

printUserName(new UserName('Alice')); // -> Alice

ここで新たにユーザの住所を意味するバリューオブジェクトを追加しました。

class UserAddress {
  public readonly value: string;

  public constructor(value: string) {
    // 住所は 5 文字以上の必要がある
    if (value.length < 5) {
      throw new Error();
    }
    this.value = value;
  }
}

このとき printUserName 関数には UserName だけでなく UserAddress も渡すことができます。

function printUserName(userName: UserName): void {
  console.log(userName.value);
}

printUserName(new UserName('Jane Doe')); // -> Jane Doe

printUserName(new UserAddress('Tokyo, Japan')); // -> Tokyo, Japan

引数として UserName 型を要求しているところに UserAddress 型を渡すことができることに違和感を感じる人もいるでしょう。

これは TypeScript が構造的部分型を採用しているためです。

詳細はこちらをご参照ください。

https://typescriptbook.jp/reference/values-types-variables/structural-subtyping

TypeScript で公称型のクラスを作成する #

今回の例のように TypeScript であっても公称型のクラスが欲しいときがあります。

そんなときは何かしらのプロパティを非パブリックにすることで実現できます。

https://typescriptbook.jp/reference/object-oriented/class/class-nominality

TypeScript では、クラスに 1 つでも非パブリックなプロパティがあると、そのクラスだけ構造的部分型ではなく公称型(nominal typing)になります。

さきほどの UserName に private フィールドを追加します。

class UserName {
  // @ts-ignore
  private _nominal: never; // 👈 追加

  public readonly value: string;

  public constructor(value: string) {
    // ユーザ名は 3 文字以上でなければいけない
    if (value.length < 3) {
      throw new Error();
    }
    this.value = value;
  }
}

すると引数で UserAddress を渡しているところはエラーに変わります。

function printUserName(userName: UserName): void {
  console.log(userName.value);
}

printUserName(new UserName('Jane Doe')); // -> Jane Doe

printUserName(new UserAddress('Tokyo, Japan')); // Argument of type 'UserAddress' is not assignable to parameter of type 'UserName'. Property '_nominal' is missing in type 'UserAddress' but required in type 'UserName'.ts(2345)

(なお上記では UserName クラスだけに新規フィールドを追加していますが、UserAddress クラスにも同様に private _nominal: never; を追加したとしてもエラーになり続けます。)

ということで期待する動きは実現できましたが、実際は利用されない private なフィールドを無理矢理追加するのはいささか強引なので、実際には次のようにするのがよいでしょう。

class UserName {
  private readonly _value: string;

  public constructor(value: string) {
    // ユーザ名は 3 文字以上の必要がある
    if (value.length < 3) {
      throw new Error();
    }
    this._value = value;
  }

  public get value(): string {
    return this._value;
  }
}

アンダースコアの名称でない方が良い場合は、JavaScript のプライベートフィールド(#)を使うと良いです。

class UserName {
  #value: string;

  public constructor(value: string) {
    // ユーザ名は 3 文字以上の必要がある
    if (value.length < 3) {
      throw new Error();
    }
    this.#value = value;
  }

  public get value(): string {
    return this.#value;
  }
}