型引数の制約
TypeScriptではジェネリクスの型引数を特定の型に限定することができます。
ジェネリクス型引数で直面する問題
changeBackgroundColor()
という関数を例に考えてみます。この関数は指定されたHTML要素の背景色を変更して、そのHTML要素を返す関数です。
ジェネリクス型T
を定義することでHTMLButtonElement
やHTMLDivElement
などの任意のHTML要素を受け取れるようにしています。
ts
functionchangeBackgroundColor <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 ;}
ts
functionchangeBackgroundColor <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
を使えばコンパイルエラーを回避することは可能ですが型のチェックがされません。将来バグが発生する危険性もあるので、できる限り避けたいところです。
ts
functionchangeBackgroundColor <T >(element :T ) {// any に型アサーションすればコンパイルエラーは回避できる// 型チェックされないのでバグの可能性(element as any).style .backgroundColor = "red";returnelement ;}
ts
functionchangeBackgroundColor <T >(element :T ) {// any に型アサーションすればコンパイルエラーは回避できる// 型チェックされないのでバグの可能性(element as any).style .backgroundColor = "red";returnelement ;}
型引数に制約をつける
TypeScriptではextends
キーワードを用いることでジェネリクスの型T
を特定の型に限定することができます。
今回の例では<T extends HTMLElement>
とすることで型T
は必ずHTMLElement
またはそのサブタイプのHTMLButtonElement
やHTMLDivElement
であることが保証されるためstyle
プロパティに安全にアクセスできるようになります。
ts
functionchangeBackgroundColor <T extendsHTMLElement >(element :T ) {element .style .backgroundColor = "red";returnelement ;}
ts
functionchangeBackgroundColor <T extendsHTMLElement >(element :T ) {element .style .backgroundColor = "red";returnelement ;}
このextends
キーワードはインターフェースに対しても使います。インターフェースは実装のときはimplements
キーワードを使いますが型引数に使うときはimplements
を使わず同様にextends
を使います。
ts
interfaceValueObject <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 ;}//...}
ts
interfaceValueObject <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
でなければなりません。