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

## やりたいこと
クラス`Class1`、`Class2`、`Class3`があります。
新たに作るクラス`ClassA`は`Class1`、`Class2`、`Class3`のオプショナルプロパティを持ちます。
`ClassA`に以下の要件を満たすコンストラクタを作ります。
* 引数の型は`Class1`、`Class2`、`Class3`
* 引数0〜3個
* 引数の順番を問わない

```typescript
// 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`

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

前準備として、`Name`、`DateOfBirth`、`Address`クラスを定義します。
<details><summary>クラスの定義</summary>

```typescript
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;
  }
}
```
```typescript
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;
  }
}
```
```typescript
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;
  }
}
```

</details>

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

:::note warn
この実装方法は問題点があります。詳細は[問題点](#問題点)をご参照ください。
:::

```typescript
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個
```typescript
const person1 = new Person();
person1.log();
// Output: 
// "name=undefined"
// "dateOfBirth=undefined"
// "address=undefined"
```

(OK)引数1個
```typescript
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個
```typescript
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)異なる順番
```typescript
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)対象外の引数の型
```typescript
const person5 = new Person(null);
// Argument of type 'null' is not assignable to parameter of type 'AllowedParameter'.(2345)
```

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

```typescript
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 さんよりコメントを頂きました。
型引数、`extends`、`Exclude`を利用した方法で、やりたいことを全部実現できる上に問題点も解消できます。
詳しくはコメント欄をご覧ください。

## 参考

https://timmousk.com/blog/typescript-multiple-constructors/

https://typescriptbook.jp/reference/functions/overload-functions

https://typescript-jp.gitbook.io/deep-dive/type-system/typeguard