メインコンテンツまでスキップ

型引数の制約

TypeScriptではジェネリクスの型引数を特定の型に限定することができます。

ジェネリクス型引数で直面する問題

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

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

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

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

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

型引数に制約をつける

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

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

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

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

ts
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;
}
 
//...
}
ts
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でなければなりません。