TypeScript に関する Tips をメモっていきます。

クラス名の取得方法

取得方法

  • クラス内から取得する場合
    • static method 内: this.name
    • instance method 内: this.constructor.name
  • クラス外で取得する場合
    • クラス名.name
    • インスタンス.constructor.name

テストコード

以下のテストコードで検証しました。
継承してても問題なくクラス名を取得できました。

class Parent {
  constructor(public name: string) {}

  static classNameInStaticMethod(): string {
    return this.name;
  }

  classNameInInstanceMethod(): string {
    return this.constructor.name;
  }
}

class Child extends Parent {}

test('parent', () => {
  expect(Parent.classNameInStaticMethod()).toBe('Parent');
  expect(new Parent('prament').classNameInInstanceMethod()).toBe('Parent');
  expect(new Parent('prament').constructor.name).toBe('Parent');
  expect(Parent.name).toBe('Parent');
});

test('child', () => {
  expect(Child.classNameInStaticMethod()).toBe('Child');
  expect(new Child('child').classNameInInstanceMethod()).toBe('Child');
  expect(new Child('prament').constructor.name).toBe('Child');
  expect(Child.name).toBe('Child');
});

webpack でトランスパイルする場合の注意点

webpack でトランスパイルすると、クラス名や関数名は短縮されて r などの1文字のアルファベットに置き換わってしまいます。そのため、クラス名を取得したいケースで不都合が起きます。
そのような場合は、webpack.config.jsの設定で、クラス名をキープするように設定することで回避します。

const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          keep_classnames: true,
        },
      }),
    ],
  },
}

拡張可能な Enum を作る

普通の Enum

Java とかだと Enum は一つの定数に対して、複数のプロパティ値を持たせたり、関数を定義したり自由にできましたが、普通の Enum だと、複数プロパティを持たせたりはできないみたいです。
(関数定義だけなら、 namespace を使ってできるみたいです)。

enum Length1 {
  L,
  S,
}

enum Length2 {
  L = 'Long', // value も指定
  S = 'Short',
}

test('normal', async () => {
  expect(Length1.L).toBe(Length1.L);
  expect(Length1.S).toBe(Length1.S);
  expect(Length1.L.valueOf()).toBe(Length1.L);
  expect(Length1.S.valueOf()).toBe(Length1.S);
  expect(Length2.L).toBe(Length2.L);
  expect(Length2.S).toBe(Length2.S);
  expect(Length2.L.valueOf()).toBe('Long');
  expect(Length2.S.toString()).toBe('Short');
});

独自 Enum クラス

複数プロパティを定義したり、ファクトリメソッド作ったり、自由にできるようにした独自 Enum クラスです。

/** サイズ規定クラス */
class Size {
  // 定数インスタンスを static 変数の Array として保持
  private static readonly values = new Array<Size>();

  // 定数インスタンスを作成。変更されると困るので readonly 必須
  static readonly S = new Size('S', 'Small');
  static readonly M = new Size('M', 'Medium');
  static readonly L = new Size('L', 'Large');

  /** コンストラクタ。は外部から実行されると困るので private 装飾*/
  private constructor(readonly name: string, readonly fullname: string) {
    Size.values.push(this);
  }

  /** 名前を使用したファクトリメソッド */
  static of(name: string): Size {
    const size: Size | undefined = Size.values.find((v) => v.name === name);
    if (size === undefined) throw new Error(`${name} is not supported.`);
    return size;
  }

  /** フルネームを使用したファクトリメソッド */
  static from(fullname: string): Size {
    const size: Size | undefined = Size.values.find((v) => v.fullname === fullname);
    if (size === undefined) throw new Error(`${fullname} is not supported.`);
    return size;
  }

  // インスタンスメソッドの例
  toString(): string {
    return `${this.name}: ${this.fullname}`;
  }
}

// 正常系
test('normal', async () => {
  console.log(Size.S); // Size { name: 'S', fullname: 'Small' }
  expect(Size.of('M')).toBe(Size.M);
  expect(Size.from('Large')).toBe(Size.L);
  expect(Size.S.toString()).toBe('S: Small');
});

// 異常系
test('abnormal', async () => {
  expect(() => Size.of('XX')).toThrowError();
  expect(() => Size.from('XX')).toThrowError();
  // new Size('XL', 'Xstra Large'); compile error
});

独自 Enum に規定クラスを導入

Enum クラスを何種類か作りたくなった時は、共通部分を規定クラスとして定義し、継承したくなると思います。
今回の例では、名前とフルネームを持つ規定クラスを作ってみました。

/**
 * 名前とフルネームを持つ Enum クラスの規定クラス
 * 直接インスタンス生成されないように abstract クラスとして定義
 */
abstract class NameAndFullname {
  // 定数インスタンスを保持。ただし、全てのサブクラスの定数インスタンスが入ってくるので要注意。今回の例では、Sex.M, Sex.F, Color.B, Color.Wが入ってくる
  private static values = new Array<NameAndFullname>();

  // 継承先から利用できるように protected 修飾
  protected constructor(readonly name: string, readonly fullname: string) {
    NameAndFullname.values.push(this);
  }

  /** 名前を使用したファクトリメソッド */
  static of(name: string): NameAndFullname {
    // 別のサブクラスで同名の定数があるとまずいので、クラス名もチェック
    const nameAndFullname = NameAndFullname.values.find((v) => v.constructor.name === this.name && v.name === name);
    if (nameAndFullname === undefined) throw new Error(`${this.name}.${name} is not supported.`);
    return nameAndFullname;
  }

  /** フルネームを使用したファクトリメソッド */
  static from(fullname: string): NameAndFullname {
    const nameAndFullname = NameAndFullname.values.find((v) => v.constructor.name === this.name && v.fullname === fullname);
    if (nameAndFullname === undefined) throw new Error(`${this.name}.${fullname} is not supported.`);
    return nameAndFullname;
  }

  toString(): string {
    return this.fullname;
  }
}

/** 基底クラスをそのまま利用 */
class Sex extends NameAndFullname {
  static readonly M = new Sex('M', 'Male');
  static readonly F = new Sex('F', 'Female');
}

/** 基底クラスを一部拡張 */
class Color extends NameAndFullname {
  private hex: string; // Color 特有のプロパティ
  static readonly B = new Color('B', 'Black', '#000000');
  static readonly W = new Color('W', 'White', '#FFFFFF');

  // constructorを上書き
  constructor(name: string, fullname: string, hex: string) {
    super(name, fullname);
    this.hex = hex;
  }

  // toString を上書き
  toString(): string {
    return `${this.fullname}(${this.hex})`; // hex も出力
  }
}

test('create by factory method', async () => {
  expect(Sex.of('M')).toBe(Sex.M);
  expect(Sex.of('F')).toBe(Sex.F);
  expect(Sex.from('Male')).toBe(Sex.M);
  expect(Sex.from('Female')).toBe(Sex.F);
  expect(Color.of('B')).toBe(Color.B);
  expect(Color.of('W')).toBe(Color.W);
  expect(Color.from('Black')).toBe(Color.B);
  expect(Color.from('White')).toBe(Color.W);
});

test('toString returns fullname', async () => {
  expect(Sex.of('M').toString()).toBe('Male');
  expect(Sex.of('F').toString()).toBe('Female');
  expect(Color.of('B').toString()).toBe('Black(#000000)'); // ちゃんと上書きした toString() が利用されている
  expect(Color.of('W').toString()).toBe('White(#FFFFFF)'); // ちゃんと上書きした toString() が利用されている
});

メンバー変数の定義方法

メンバー変数の定義方法には、普通にクラス直下に定義する方法と、コンストラクタで定義する方法があります。

/** メンバー変数を普通に定義するサンプル */
class Member1 {
  public a: string;
  protected b: string;
  private c: string;
  readonly d: string; // public readonly と同じ
  constructor(a: string, b: string, c: string, d: string) {
    this.a = a;
    this.b = b;
    this.c = c;
    this.d = d;
  }
}

/** コンストラクタでメンバー変数を定義するサンプル */
class Member2 {
  constructor(public a: string, protected b: string, private c: string, readonly d: string) {}
}

test('create instance member1', () => {
  const member1 = new Member1('aa', 'bb', 'cc', 'dd');
  expect(member1.a).toBe('aa');
  // expect(member1.b).toBe('bb'); compile error
  // expect(member1.c).toBe('cc'); compile error
  expect(member1.d).toBe('dd');
});

test('create instance member2', () => {
  const member2 = new Member2('aa', 'bb', 'cc', 'dd');
  expect(member2.a).toBe('aa');
  // expect(member2.b).toBe('bb'); compile error
  // expect(member2.c).toBe('cc'); compile error
  expect(member2.d).toBe('dd');
});

一応、アクセス修飾子の意味を書いておくと

  • public: クラス外からアクセスできる
  • protected: クラス外からアクセスできないけど、継承したクラス内からはアクセスできる
  • private: クラス外および継承したクラス内からアクセスできない。クラス内のみアクセスできる。

ちなみに、readonlyは、変更可能かどうかを示していて、このケースはpublic readonlyとして定義されたものと同等の扱いになります。

親クラスの配列の static メンバー変数に子クラスで値を追加

わざわざ書くほどのものでもなさそうだけど、親クラスで定義した配列の static メンバー変数に子クラスで値を追加したいケースがあったのでメモっておきます。

class ParentMember {
  static readonly keys = ['aa', 'bb'];
}

class ChildMember1 extends ParentMember {
  static readonly keys = ParentMember.keys.concat(['cc1', 'dd1']);
}

class ChildMember2 extends ParentMember {
  static readonly keys = ParentMember.keys.concat(['cc2', 'dd2']);
}

test('overwrite? parent keys', () => {
  expect(ParentMember.keys).toStrictEqual(['aa', 'bb']);
  expect(ChildMember1.keys).toStrictEqual(['aa', 'bb', 'cc1', 'dd1']);
  expect(ChildMember2.keys).toStrictEqual(['aa', 'bb', 'cc2', 'dd2']);
});

事前にプロパティが分からない場合のコンストラクタ

外部から渡ってきた値を元にインスタンス生成したいようなケースでは、コンストラクタの引数で個別にプロパティを指定することが難しいケースがあります。

そのようなケースは、メンバー変数を任意(undefinedを許可)として定義しておき、Partialを引数に取るコンストラクタを定義するとよかったです。

class PartialConstructorSample {
  readonly aa?: string; // readonly aa: string | undefined;と同等
  readonly bb?: string;
  readonly cc?: string;

  constructor(init?: Partial<PartialConstructorSample>) {
    Object.assign(this, init);
  }
}

test('create instance', () => {
  expect(new PartialConstructorSample({ aa: 'aaa' })).toBeInstanceOf(PartialConstructorSample);
  expect(new PartialConstructorSample({ aa: 'aaa', bb: 'bbb' })).toBeInstanceOf(PartialConstructorSample);
  expect(new PartialConstructorSample({ aa: 'aaa', cc: 'ccc' })).toBeInstanceOf(PartialConstructorSample);
  // expect(new PartialConstructorSample({ aa: 'aaa', dd: 'zzz' })).toBeInstanceOf(PartialConstructorSample); compile error
});