ゴール:「毎回 <T, string> と書かなくても済む“親切なジェネリクス”を書けるようになる
デフォルト型パラメータは一言でいうと、
「ジェネリクスに“デフォルトの型”を用意しておく仕組み」
です。
関数の引数にデフォルト値をつけるのと同じで、
「型パラメータを省略したときに、代わりに使われる型」を決めておけます。
これが分かると、
- 8割のケースでは型を省略してスッキリ書ける
- 残り2割の特殊なケースでは、明示的に型を指定して上書きできる
という、使い勝手のいいジェネリクスが書けるようになります。
まずは「デフォルトなし」のジェネリクスから
シンプルなジェネリックインターフェース
例えば、API のレスポンスを表すジェネリック型を考えます。
interface ApiResponse<T> {
data: T;
success: boolean;
errorMessage?: string;
}
TypeScript使うときは、毎回 T を指定する必要があります。
type User = { id: number; name: string };
const res1: ApiResponse<User> = {
data: { id: 1, name: "Taro" },
success: true,
};
const res2: ApiResponse<string> = {
data: "OK",
success: true,
};
TypeScriptこれはこれで正しいのですが、
「ほとんどのケースで T = unknown でいい」とか
「だいたい string を返す API ばかり」みたいなとき、
毎回書くのがちょっとダルくなります。
デフォルト型パラメータの基本構文
T = 〜 と書くだけ
デフォルト型パラメータは、こう書きます。
interface ApiResponse<T = unknown> {
data: T;
success: boolean;
errorMessage?: string;
}
TypeScript<T = unknown> がポイントです。
意味としては、
「T を指定しなかったときは、T は unknown とみなす」
という宣言です。
使い方はこう変わります。
// T を省略 → T は unknown になる
const res1: ApiResponse = {
data: { id: 1, name: "Taro" },
success: true,
};
// T を明示 → もちろん上書きできる
const res2: ApiResponse<string> = {
data: "OK",
success: true,
};
TypeScript関数の引数でいうと、
function f(x: number = 1) {}
TypeScriptと同じノリで、
「指定されなかったらこれを使うね」
という“型版のデフォルト値”だと思ってください。
ジェネリック関数でのデフォルト型パラメータ
ログ関数の例
ジェネリック関数にもデフォルト型パラメータをつけられます。
function logValue<T = string>(value: T): void {
console.log(value);
}
TypeScriptこの関数は、
- 型パラメータ T を持つ
- ただし、T が省略されたら T = string とみなす
という意味になります。
呼び出し側の挙動を見てみましょう。
logValue("hello"); // T は string(デフォルト or 推論)
logValue<number>(123); // T を明示的に number に上書き
TypeScriptここで重要なのは、
「デフォルトは“指定されなかったときの初期値”であって、
明示されたときにはちゃんと上書きされる」
という点です。
「ふつうは string を扱うけど、たまに number も扱いたい」
みたいな関数に、すごく相性がいいです。
デフォルト型パラメータ+制約
制約(extends)とデフォルトは一緒に使えます。
function getLength<T extends { length: number } = string>(value: T): number {
return value.length;
}
TypeScriptこれは、
- T は
{ length: number }を満たす型に限る - かつ、T が省略されたら T = string とみなす
という意味です。
getLength("hello"); // T は string(デフォルト)
getLength([1, 2, 3]); // T は number[](推論)
getLength<{ length: number }>({ length: 10 }); // T を明示
TypeScript「デフォルト」と「制約」は別物で、
- 制約:T が満たさなければいけない条件
- デフォルト:T が省略されたときに使う型
と覚えておくと整理しやすいです。
複数型パラメータとデフォルト
2つ目以降にデフォルトをつけるのが定番
複数型パラメータがある場合、
よくあるのは「2つ目以降にデフォルトをつける」パターンです。
interface Result<TData = unknown, TError = string> {
data?: TData;
error?: TError;
}
TypeScript使い方はこうなります。
// 何も指定しない → TData = unknown, TError = string
const r1: Result = {
error: "Something went wrong",
};
// データ型だけ指定 → エラー型はデフォルトの string
const r2: Result<number> = {
data: 123,
};
// 両方指定して上書き
const r3: Result<number, { code: number; message: string }> = {
error: { code: 500, message: "Server error" },
};
TypeScriptここでのポイントは、
「よく変わる型だけを先に置き、あまり変えない型にはデフォルトをつける」
という設計です。
こうすると、呼び出し側のコードがかなりスッキリします。
デフォルト型パラメータの順番ルール
TypeScript では、
「デフォルトのない型パラメータの後ろに、デフォルト付きの型パラメータを置く」
のが基本です。
OK な例:
interface Example<T, U = string> {}
TypeScriptやりがちな NG パターンはこれです。
// これはダメ
interface BadExample<T = string, U> {}
TypeScript「T にデフォルトがあるのに、後ろの U にはない」
という並びは許されません。
イメージとしては、
関数の引数のデフォルトと同じです。
function f(x = 1, y: number) {} // ダメ
function g(x: number, y = 1) {} // OK
TypeScriptデフォルト型パラメータが“効いてくる”場面
8割のケースが同じ型、というとき
例えば、イベントハンドラの型を考えます。
type Handler<TEvent = MouseEvent> = (event: TEvent) => void;
TypeScriptこうしておくと、
const onClick: Handler = (e) => {
// e: MouseEvent
};
const onKeyDown: Handler<KeyboardEvent> = (e) => {
// e: KeyboardEvent
};
TypeScript「デフォルトは MouseEvent だけど、必要なら KeyboardEvent などに変えられる」
という、使い勝手のいい型になります。
「ほとんどのケースで同じ型を使うけど、たまに変えたい」
というときに、デフォルト型パラメータは本当に気持ちよくハマります。
ライブラリ的な型を自作するとき
自分でユーティリティ型やラッパー型を作るとき、
デフォルト型パラメータをつけておくと、
「使う側のコードのノイズ」をかなり減らせます。
例えば、非同期処理の結果を表す型。
type AsyncResult<TData = void, TError = Error> = Promise<{
data?: TData;
error?: TError;
}>;
TypeScript使うときはこうです。
// 何も返さない非同期処理(成功/失敗だけ知りたい)
const r1: AsyncResult = /* ... */;
// number を返す非同期処理
const r2: AsyncResult<number> = /* ... */;
// エラー型もカスタムしたい場合
const r3: AsyncResult<number, { code: number; message: string }> = /* ... */;
TypeScript「よくあるパターン」をデフォルトにしておくことで、
呼び出し側の型指定が最小限で済みます。
まとめ:デフォルト型パラメータを自分の言葉で説明すると
最後に、あなた自身の言葉でこう整理してみてください。
デフォルト型パラメータは、
「ジェネリクスの型パラメータに“省略時の標準値”をつける仕組み」。
<T = string> のように書くことで、
T が指定されなかったときに自動的に string が使われる。
よく変わる型にはデフォルトをつけず、
あまり変えない型にはデフォルトをつけることで、
- ふだんは型指定を省略してスッキリ書ける
- 必要なときだけ明示的に型を上書きできる
という、使いやすいジェネリクスになる。
まずは次の 2 つを、自分の手で書いて試してみてください。
interface ApiResponse<T = unknown> { data: T; success: boolean; }
type Handler<TEvent = MouseEvent> = (event: TEvent) => void;
TypeScriptそして、型パラメータを省略した場合と、
明示的に指定した場合の両方を使ってみて、
「デフォルトは“省略したときだけ効く型の初期値”なんだな」
という感覚をつかんでみてください。
