やりたいこと
クラスClass1
、Class2
、Class3
があります。
新たに作るクラスClassA
はClass1
、Class2
、Class3
のオプショナルプロパティを持ちます。
ClassA
に以下の要件を満たすコンストラクタを作ります。
- 引数の型は
Class1
、Class2
、Class3
- 引数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
に置き換えて実装していきます。
前準備として、Name
、DateOfBirth
、Address
クラスを定義します。
クラスの定義
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)
問題点
引数の型がName
、DateOfBirth
、Address
のいずれであれば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 さんよりコメントを頂きました。
型引数、extends
、Exclude
を利用した方法で、やりたいことを全部実現できる上に問題点も解消できます。
詳しくはコメント欄をご覧ください。
参考