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