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