型引数の制約
TypeScriptではジェネリクスの型引数を特定の型に限定することができます。
ジェネリクス型引数で直面する問題
changeBackgroundColor()という関数を例に考えてみます。この関数は指定されたHTML要素の背景色を変更して、そのHTML要素を返す関数です。
ジェネリクス型Tを定義することでHTMLButtonElementやHTMLDivElementなどの任意のHTML要素を受け取れるようにしています。
tsfunctionchangeBackgroundColor <T >(element :T ) {// Property 'style' does not exist on type 'T'.(2339)Property 'style' does not exist on type 'T'.2339Property 'style' does not exist on type 'T'.element .. style backgroundColor = "red";returnelement ;}
tsfunctionchangeBackgroundColor <T >(element :T ) {// Property 'style' does not exist on type 'T'.(2339)Property 'style' does not exist on type 'T'.2339Property 'style' does not exist on type 'T'.element .. style backgroundColor = "red";returnelement ;}
このコードはコンパイルに失敗します。ジェネリクスの型Tは任意の型が指定可能なので、渡す型によってはstyleプロパティが存在しない場合があるからです。コンパイラは存在しないプロパティへの参照が発生する可能性を検知してコンパイルエラーとしているのです。
anyを使えばコンパイルエラーを回避することは可能ですが型のチェックがされません。将来バグが発生する危険性もあるので、できる限り避けたいところです。
tsfunctionchangeBackgroundColor <T >(element :T ) {// any に型アサーションすればコンパイルエラーは回避できる// 型チェックされないのでバグの可能性(element as any).style .backgroundColor = "red";returnelement ;}
tsfunctionchangeBackgroundColor <T >(element :T ) {// any に型アサーションすればコンパイルエラーは回避できる// 型チェックされないのでバグの可能性(element as any).style .backgroundColor = "red";returnelement ;}
型引数に制約をつける
TypeScriptではextendsキーワードを用いることでジェネリクスの型Tを特定の型に限定することができます。
今回の例では<T extends HTMLElement>とすることで型Tは必ずHTMLElementまたはそのサブタイプのHTMLButtonElementやHTMLDivElementであることが保証されるためstyleプロパティに安全にアクセスできるようになります。
tsfunctionchangeBackgroundColor <T extendsHTMLElement >(element :T ) {element .style .backgroundColor = "red";returnelement ;}
tsfunctionchangeBackgroundColor <T extendsHTMLElement >(element :T ) {element .style .backgroundColor = "red";returnelement ;}
このextendsキーワードはインターフェースに対しても使います。インターフェースは実装のときはimplementsキーワードを使いますが型引数に使うときはimplementsを使わず同様にextendsを使います。
tsinterfaceValueObject <T > {value :T ;toString (): string;}classUserID implementsValueObject <number> {publicvalue : number;public constructor(value : number) {this.value =value ;}publictoString (): string {return `${this.value }`;}}classEntity <ID extendsValueObject <unknown>> {privateid :ID ;public constructor(id :ID ) {this.id =id ;}//...}
tsinterfaceValueObject <T > {value :T ;toString (): string;}classUserID implementsValueObject <number> {publicvalue : number;public constructor(value : number) {this.value =value ;}publictoString (): string {return `${this.value }`;}}classEntity <ID extendsValueObject <unknown>> {privateid :ID ;public constructor(id :ID ) {this.id =id ;}//...}
EntityクラスはValueObjectインターフェースを実装しているクラスをIDとして受ける構造になっていますが19行目にあるようにこのときの型引数の制約はimplementsではなくextendsでなければなりません。