TypeScriptのジェネリクスに関するよくあるエラーとその対処法
生徒
「先生、TypeScriptでジェネリクスを使ってみたらエラーが出てしまいました…。何が悪いのかわからないんです。」
先生
「なるほど。TypeScriptのジェネリクスは便利ですが、最初はエラーが出やすいポイントがいくつかありますね。」
生徒
「どうすれば直せるか知りたいです!」
先生
「それでは、初心者がよくつまずくジェネリクスのエラーと、その対処法を一つずつ見ていきましょう。」
1. ジェネリクスの型パラメータが推論できないエラー
TypeScriptでよくあるのが、関数を呼び出したときに「ジェネリクスの型を推論できない」というエラーです。これは、TypeScriptが「Tとは何の型か」を判断できないときに発生します。
function identity<T>(value: T): T {
return value;
}
const result = identity(); // エラー!
型引数を推論できません。引数を指定してください。
このエラーの原因は、identity()関数を呼び出す際に引数が渡されていないため、TypeScriptが「Tがどの型なのか」を判断できないからです。対処法は2つあります。
- 引数を正しく渡す
- 明示的に型パラメータを指定する
// 対処法① 引数を渡す
const result1 = identity("Hello");
// 対処法② 明示的に型を指定する
const result2 = identity<number>(100);
このように、型推論がうまく働かない場合は、型を明示的に書くことでエラーを回避できます。
2. 型制約(extends)の指定ミスによるエラー
ジェネリクスでは、extendsを使って型に制約をかけることができます。しかし、制約の内容と一致しない型を渡すとエラーになります。
function getName<T extends { name: string }>(obj: T) {
return obj.name;
}
getName({ age: 30 }); // エラー!
型 '{ age: number; }' の引数を型 '{ name: string; }' のパラメータに割り当てることはできません。
このエラーは、「nameプロパティを持つオブジェクト」だけを受け取る制約をかけているのに、ageしかないオブジェクトを渡しているのが原因です。
正しくは、次のようにnameプロパティを含める必要があります。
getName({ name: "田中", age: 30 }); // OK
制約(extends)を使うと、型の安全性を高めることができますが、条件に合わない型を渡さないように注意しましょう。
3. 型パラメータの順序によるエラー
複数の型パラメータを使うとき、順番や関係性が間違っているとエラーになることがあります。
function merge<T, U extends T>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
merge({ name: "佐藤" }, { age: 20 }); // エラー!
型 '{ age: number; }' の引数を型 'T' のパラメータに割り当てることはできません。
この場合、U extends Tという制約をつけたため、「UはTを継承していなければならない」ルールが働いています。しかし、{ age: 20 }は{ name: "佐藤" }を継承していないためエラーになってしまいます。
正しく動かすには、次のように制約を外すか、型定義を見直します。
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
merge({ name: "佐藤" }, { age: 20 }); // OK
ジェネリクスの型関係は柔軟ですが、extendsを付けると上下関係が発生するため注意が必要です。
4. 関数の戻り値の型が一致しないエラー
ジェネリクス関数で戻り値を間違えると、「型が一致しません」というエラーが出ます。
function wrapValue<T>(value: T): T[] {
return value; // エラー!
}
型 'T' を型 'T[]' に割り当てることはできません。
この関数は「T型の配列(T[])」を返すはずなのに、実際にはTそのものを返しているためエラーになります。正しくは配列にして返す必要があります。
function wrapValue<T>(value: T): T[] {
return [value]; // OK
}
戻り値の型と実際の返り値の型は常に一致させましょう。ジェネリクスでは、TypeScriptが自動で推論しますが、間違えた場合は厳密にチェックされます。
5. any型との組み合わせで型推論が失敗するエラー
TypeScriptのジェネリクスでは、any型を混ぜると型推論が崩れることがあります。anyは「どんな型でもOK」という意味なので、TypeScriptが安全な型を判断できなくなってしまうのです。
function showValue<T>(value: T) {
console.log(value);
}
let data: any = "テスト";
showValue(data); // 型安全ではない
この場合、TypeScriptはTをanyとして扱うため、どんな値でも通ってしまい、型のチェックが無効になります。安全に扱いたい場合は、型を明示的に指定するか、unknown型を使う方法があります。
let data2: unknown = "テスト";
showValue<string>(data2 as string); // OK
unknown型はanyよりも安全で、型チェックを残したまま扱えるため、ジェネリクスと組み合わせるときはおすすめです。
6. keyofや条件付き型と組み合わせたときのエラー
ジェネリクスはkeyofや条件付き型(Conditional Types)と組み合わせることが多いですが、型の指定を間違えるとエラーが出やすくなります。
function getProp<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const user = { name: "山田", age: 25 };
getProp(user, "gender"); // エラー!
型 '"gender"' の引数を型 '"name" | "age"' のパラメータに割り当てることはできません。
この場合、keyof Tは"name" | "age"なので、それ以外のキー(ここでは"gender")を指定するとエラーになります。
正しくは、オブジェクトに存在するキーを指定します。
getProp(user, "age"); // OK
このように、TypeScriptのジェネリクスは型安全を守るために厳しくチェックしてくれます。最初はエラーが多く見えるかもしれませんが、どれも安全なコードを書くためのサポートです。
まとめ
TypeScriptのジェネリクスで理解しておきたい全体像
ここまで、TypeScriptのジェネリクスに関するよくあるエラーと対処法について見てきました。ジェネリクスは、関数やクラス、インターフェースをより汎用的かつ安全に設計できる強力な仕組みです。その一方で、型推論や型制約、戻り値の指定、keyofや条件付き型との組み合わせなど、理解が浅い状態で使うとエラーが発生しやすいという特徴もあります。
特に初心者の段階では、「なぜこのエラーが出ているのか」「TypeScriptは何を判断できていないのか」を意識することが重要です。ジェネリクスのエラーは、単なる記述ミスではなく、型安全を守るためにTypeScriptが警告を出しているケースがほとんどです。そのため、エラーメッセージを丁寧に読み解くことが、TypeScriptの理解を深める近道になります。
エラーから学ぶジェネリクスの正しい考え方
型パラメータを推論できないエラーでは、「情報が足りないと型は決まらない」という基本原則を学びました。extendsによる型制約のエラーでは、「制約はルールであり、必ず満たす必要がある」ことを理解できたはずです。また、複数の型パラメータを扱う場合は、それぞれの関係性や上下関係を正しく設計しないと、意図しないエラーにつながることも確認しました。
戻り値の型が一致しないエラーは、ジェネリクスに限らずTypeScript全体に共通する重要なポイントです。関数の戻り値は宣言した型と完全に一致している必要があり、ジェネリクスであってもそのルールは変わりません。さらに、any型との併用は型推論を崩す原因になるため、unknown型を使って安全性を保つ考え方も押さえておきたいところです。
実務で役立つジェネリクスの書き方例
class Box<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
const stringBox = new Box<string>("サンプル");
const result = stringBox.getValue();
このようにクラスとジェネリクスを組み合わせることで、型安全を保ちながら再利用性の高い設計が可能になります。関数だけでなく、クラスやインターフェースでもジェネリクスは頻繁に使われるため、今回紹介したエラーの考え方はそのまま応用できます。
生徒
「最初はジェネリクスのエラーがたくさん出て混乱していましたが、型を安全にするための仕組みだと分かってきました。エラーの意味を見るのが少し楽しくなってきました。」
先生
「それはとても良い変化ですね。TypeScriptのジェネリクスは、エラーを通して型の考え方を学べるのが大きな特徴です。エラーを怖がらずに、なぜそう判断されたのかを考える癖をつけると、自然と理解が深まりますよ。」
生徒
「extendsやkeyofも最初は難しかったですが、制約やルールだと考えると整理しやすかったです。実際の開発でも使えそうな気がします。」
先生
「その感覚が大切です。ジェネリクスはTypeScriptの中でも実務でよく使われる機能なので、今回学んだエラーと対処法を覚えておけば、開発現場でも必ず役に立ちます。」
生徒
「これからは、エラーをただ直すだけじゃなくて、TypeScriptが何を守ろうとしているのかも意識して書いてみます。」
先生
「それができれば、もう立派なTypeScriptの書き手ですね。ジェネリクスを使いこなせるようになると、コードの品質も一段上がりますよ。」