1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[TypeScript]任意の長さ、任意の順番の引数を指定できるコンストラクタ

Last updated at Posted at 2024-01-30

やりたいこと

クラスClass1Class2Class3があります。
新たに作るクラスClassAClass1Class2Class3のオプショナルプロパティを持ちます。
ClassAに以下の要件を満たすコンストラクタを作ります。

  • 引数の型はClass1Class2Class3
  • 引数0〜3個
  • 引数の順番を問わない
// class1はClass1クラスのインスタンス、class2はClass2クラスのインスタンス…とする

new ClassA(); // OK
new ClassA(class2); // OK
new ClassA(class1, class3); // OK
new ClassA(class3, class2, class1); // OK
new ClassA(class1, class2, class3); // OK
new ClassA(class4); // NG
new ClassA(class1, class2, class3, class1); // NG

実装

具体性を持たせるために、ここからは

  • Class1➡︎Name
  • Class2➡︎DateOfBirth
  • Class3➡︎Address
  • ClassA➡︎Person

に置き換えて実装していきます。

前準備として、NameDateOfBirthAddressクラスを定義します。

クラスの定義
class Name {
  private firstName: string = "";
  private middleName: string = "";
  private lastName: string = "";
  constructor(firstName?: string, middleName?: string, lastName?: string) {
    if (firstName) this.firstName = firstName;
    if (middleName) this.middleName = middleName;
    if (lastName) this.lastName = lastName;
  }
}
class DateOfBirth {
  private year: number;
  private month: number;
  private day: number;
  constructor(year: number, month: number, day: number) {
    this.year = year;
    this.month = month;
    this.day = day;
  }
}
class Address {
  private state?: string;
  private city?: string;
  private street?: string;
  constructor(state?: string, city?: string, street?: string) {
    if (state) this.state = state;
    if (city) this.city = city;
    if (street) this.street = street;
  }
}

ここからがコンストラクタの実装方法です。

この実装方法は問題点があります。詳細は問題点をご参照ください。

type AllowedParameter = Name | DateOfBirth | Address;
class Person {
  private name?: Name;
  private dateOfBirth?: DateOfBirth;
  private address?: Address;

  // コンストラクタのオーバーロード
  // 0〜3個の引数まで指定できる
  constructor();
  constructor(p1: AllowedParameter);
  constructor(p1: AllowedParameter, p2: AllowedParameter);
  constructor(p1: AllowedParameter, p2: AllowedParameter, p3: AllowedParameter);

  // コンストラクタの実体
  constructor(...params: AllowedParameter[]) {
    for (const p of params) {
      // 引数の型を逐一確認し、型が一致したものから入れていく
      if (p instanceof Name) {
        this.name = p;
        continue;
      }
      if (p instanceof DateOfBirth) {
        this.dateOfBirth = p;
        continue;
      }
      if (p instanceof Address) {
        this.address = p;
        continue;
      }
    }
  }

  public log(): void {
    console.log(`name=${JSON.stringify(this.name)}`);
    console.log(`dateOfBirth=${JSON.stringify(this.dateOfBirth)}`);
    console.log(`address=${JSON.stringify(this.address)}`);
  }
}

AllowedParameter型を定義して、Personの引数として許容する型の一覧を指定します。

コントラクタのオーバーロードを宣言します。
今回は0〜3個の引数を許容したいので、計4つを宣言しましたが、
1、3個なら2つ、0〜4個なら5つと需要によってオーバーロードの数が異なります。

コントラクタの実体には、引数を配列paramsに格納します。
paramsの中身をループして、型を逐一チェックします。
型が一致したらプロパティに代入して、continue;で次のループまで飛ばします。

結果確認用のlog()もついでに実装します。

結果

(OK)引数0個

const person1 = new Person();
person1.log();
// Output: 
// "name=undefined"
// "dateOfBirth=undefined"
// "address=undefined"

(OK)引数1個

const name2 = new Name(undefined, undefined, "Doe");
const person2 = new Person(name2);
person2.log();
// Output: 
// "name={"firstName":"","middleName":"","lastName":"Doe"}"
// "dateOfBirth=undefined"
// "address=undefined"

(OK)引数2個

const address3 = new Address("Alabama", undefined, undefined);
const name3 = new Name(undefined, undefined, "Doe");
const person3 = new Person(name3, address3);
person3.log();
// Output: 
// "name={"firstName":"","middleName":"","lastName":"Doe"}"
// "dateOfBirth=undefined"
// "address={"state":"Alabama"}"

(OK)引数3個
(OK)異なる順番

const name4 = new Name(undefined, undefined, "Doe");
const dateOfBirth4 = new DateOfBirth(1970, 1, 1);
const address4 = new Address("Alabama", undefined, undefined);
const person4A = new Person(name4, dateOfBirth4, address4);
const person4B = new Person(address4, name4, dateOfBirth4);
person4A.log();
// Output: 
// "name={"firstName":"","middleName":"","lastName":"Doe"}"
// "dateOfBirth={"year":1970,"month":1,"day":1}"
// "address={"state":"Alabama"}"
person4B.log();
// Output: 
// "name={"firstName":"","middleName":"","lastName":"Doe"}"
// "dateOfBirth={"year":1970,"month":1,"day":1}"
// "address={"state":"Alabama"}"

(NG)対象外の引数の型

const person5 = new Person(null);
// Argument of type 'null' is not assignable to parameter of type 'AllowedParameter'.(2345)

問題点

引数の型がNameDateOfBirthAddressのいずれであればOKなので、
3つともNameのようなインスタンス生成方法でもOKになります。
今回は工夫していなかったので、後勝ちで最後に指定した方の値になるが、
工夫次第で一回しか代入できないようにすることも可能です。

const name5A = new Name(undefined, undefined, "Doe");
const name5B = new Name("John", undefined, "Doe");
const person5 = new Person(name5A, name5A, name5B);
person5.log();
// Output: 
// "name={"firstName":"John","middleName":"","lastName":"Doe"}"
// "dateOfBirth=undefined"
// "address=undefined"

「引数を任意の順番で指定できるが、第一引数で指定した型は第二引数、第三引数で指定できない」ようにするいい方法はないですかね?
全パターンを列挙して宣言しておくなら可能でしょうが、あまりスマートではない…

解消法

(2024/1/31 追記)
@htsign さんよりコメントを頂きました。
型引数、extendsExcludeを利用した方法で、やりたいことを全部実現できる上に問題点も解消できます。
詳しくはコメント欄をご覧ください。

参考

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?