TypeScript ではよく関数の引数と返り値の型を定めます。こうしておくと、あるところの型の変更があった時や誤った型を使ってしまった時に問題があるか否か、あるならばどこが問題かをすぐに知ることができます。
引数と返り値の型の書き方としてよく次の様なものがあります。
import copy from 'fast-copy';
// name プロパティを持つオブジェクトを示す型
type HasName = {
name: string;
};
/** name プロパティに「さん」を追加する関数 */
function appendSantoName(hasName: HasName): HasName {
hasName = copy(hasName); // 元オブジェクトを壊さない様にする
hasName.name += 'さん';
return hasName;
}
この関数には次の様な問題が起きる欠点があります。
import copy from 'fast-copy';
// name プロパティを持つオブジェクトを示す型
type HasName = {
name: string;
};
/** name プロパティに「さん」を追加する関数 */
function appendSantoName(hasName: HasName): HasName {
hasName = copy(hasName); // 元オブジェクトを壊さない様にする
hasName.name += 'さん';
return hasName;
}
// name プロパティと他の徐不応を持つオブジェクト
const hogeUser: HasName & { hoge: string } = {
name: '浜松太郎',
hoge: 'ほげ',
};
// nameプロパティを持つオブジェクトのため appendSantoName で加工できる
const withSan = appendSantoName(hogeUser);
// 加工結果の変数の hoge プロパティが TypeScript に認識されずエラーとなる
withSan.hoge; // TS2339: Property 'hoge' does not exist on type 'HasName'
実際は元のオブジェクトの name プロパティの値が変わっただけのオブジェクトが返ってきているにも関わらず、TypeScript に元のオブジェクトにあったプロパティが存在しないと怒られます。これを解決するには返り値が引数の型を自動で参照し、それを返す様に指定す必要があります。この実装が次です。
/**
* name プロパティに「さん」を追加する関数
*/
function appendSantoName<T extends HasName>(hasName: T): T {
hasName = copy(hasName); // 元オブジェクトを壊さない様にする
hasName.name += 'さん';
return hasName;
}
appendSantoName<T extends HasName>(hasName: T): Tで引数は HasName を満たす不定の型、返り値は引数と同じ型と指定しています。これで実装を反映した型を扱えます。これが特に大事なのはライブラリなどの共通モジュールのコードです。あるプロジェクトのあるドメインに特化したコードであるならば、appendSantoName(hasName: HogeUser): HogeUserの様にあらかじめ使う想定の型全体を指定することでも同じように動きます。しかしながらこれは HogeUser の範囲から外れると途端に扱いにくくなります。もっと広い範囲のどこかしらをピンポイントで扱える様にするのであればappendSantoName<T extends HasName>(hasName: T): Tと型を指定し、関数にとって必要なだけの型を満たす引数を要求しつつ正確な返り値をの型を表現するべきです。