型パラメーターの制約

TypeScriptではジェネリクスの型パラメーターを特定の型に限定することができます。

ジェネリクス型パラメータで直面する問題

changeBackgroundColor()という関数を例に考えてみます。この関数は指定されたHTML要素の背景色を変更して、そのHTML要素を返す関数です。 ジェネリクス型Tを定義することでHTMLButtonElementHTMLDivElementなどの任意のHTML要素を受け取れるようにしています。

function changeBackgroundColor<T>(element: T) {
// Property 'style' does not exist on type 'T'.(2339)
element.style.backgroundColor = 'red';
return element;
}

このコードはコンパイルに失敗します。ジェネリクスの型Tは任意の型が指定可能なので、渡す型によってはstyleプロパティが存在しない場合があるからです。コンパイラは存在しないプロパティへの参照が発生する可能性を検知してコンパイルエラーとしているのです。

anyを使えばコンパイルエラーを回避することは可能ですが型のチェックがされません。将来バグが発生する危険性もあるので、できる限り避けたいところです。

function changeBackgroundColor<T>(element: T) {
// any にキャストすればコンパイルエラーは回避できる
// 型チェックされないのでバグの可能性
(element as any).style.backgroundColor = 'red';
return element;
}

型パラメータに制約をつける

TypeScriptではextendsキーワードを用いることでジェネリクスの型Tを特定の型に限定することができます。

今回の例では<T extends HTMLElement>とすることで型Tは必ずHTMLElementまたはそのサブタイプのHTMLButtonElementHTMLDivElementであることが保証されるためstyleプロパティに安全にアクセスできるようになります。

function changeBackgroundColor<T extends HTMLElement>(element: T) {
element.style.backgroundColor = 'red';
return element;
}

このextendsキーワードはインターフェースに対しても使います。インターフェースは実装のときはimplementsキーワードを使いますが型パラメータに使うときはimplementsを使わず同様にextendsを使います。

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でなければなりません。