TypeScript でパラレル継承階層

以前に、パラレル継承階層を実装するとダウンキャストが避けられないとの記事を書きましたが、TypeScript を使うとある程度対応できることがわかりました。

ダウンキャストをなくすためには下記の2つの機能が言語に必要となります。

  • 継承可能な型引数をサポートすること
  • ジェネリックワイルドカードをサポートすること

このうち、継承可能な型引数については TypeScript で型引数の既定値を指定できるので、イメージしていたものとは異なりますが、これを利用すれば部分的に対応できることがわかりました。

中身と入れ物の関係がパラレル継承階層になりがちなので、その例を下記に示しました。

中身は BaseContentDerivedContentDerivedContent2 と継承します。一方、入れ物はこれに対応して、BaseBoxDerivedBoxDerivedBox2 と継承します。

BaseBoxは中身を参照するcontentプロパティを持ち、これをTContent型としています。入れ物の継承に従ってこの型引数も制約の型をより継承した中身の型へと変えていきます。このようにすることで、contentへの代入は型安全が保たれます。また、サンプルプログラムでは省略していますが、各入れ物クラスの中でcontentのハンドリングの際にダウンキャストは発生しません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class BaseContent {
}

class BaseBox<TContent extends BaseContent = BaseContent> {
public content: TContent | undefined;
}

class DerivedContent extends BaseContent {
public prop1: number = 0;
}

class DerivedBox<TContent extends DerivedContent = DerivedContent> extends BaseBox<TContent> {
}

const derivedBox = new DerivedBox();
derivedBox.content = new DerivedContent();
derivedBox.content = new BaseContent(); //TS2741: Property prop1 is missing in type BaseContent but required in type DerivedContent

class DerivedContent2 extends DerivedContent {
public prop2: number = 0;
}

class DerivedBox2<TContent extends DerivedContent2 = DerivedContent2> extends DerivedBox<TContent> {

}

const derivedBox2 = new DerivedBox2();
derivedBox2.content = new DerivedContent2();
derivedBox2.content = new DerivedContent();//TS2741: Property prop2 is missing in type DerivedContent but required in type DerivedContent2
derivedBox2.content = new BaseContent();//TS2739: Type BaseContent is missing the following properties from type DerivedContent2: prop2, prop1</code></pre>

次に対応できない部分について説明します。

1
2
3
4
5
6
7
const baseBox: BaseBox = derivedBox2; //本来なら型引数が異なるので代入互換性はないはずだが代入できる。
baseBox.content = new BaseContent(); //アップキャストした参照に代入すると不正な代入が可能になる。

//型安全のためには、ジェネリックワイルドカードでないと代入できないようにする必要がある
//const baseBox: BaseBox<? extends BaseContent> = derivedBox2;
//ジェネリックワイルドカードを使った場合、下記の代入はエラーになる。
//baseBox.content = new BaseContent();

上記のコードで、DerivedContent2からBaseBoxへ継承関係にあるので、代入できると考えがちです。実際、TypeScript はこれを許可しています。しかし、継承に従って型引数が変わっているので、本来は代入互換性がないはずなのです。サンプルではダウンキャストを全く使っていないにもかかわらず、型安全でない状況が生まれます。

これを型安全にするためには前の記事でもふれたようにジェネリックワイルドカードの機能が必要です。コメント内にもしワイルドカードが使えたらどのようなコードになるかを書いています。

問題はもう一つ残っていて、入れ物の継承で中身の型である TContent が継承するたびに別物になっていることです。これも同じ型引数のまま継承させる必要があります。

言語を開発するほどの技術力と財力がありませんので、このような言語が開発されたらいいなと、願っているだけです。

参照