TypeScript | 関数・クラス・ジェネリクス:ジェネリクス基礎 - デフォルト型パラメータ

TypeScript TypeScript
スポンサーリンク

ゴール:「毎回 <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

そして、型パラメータを省略した場合と、
明示的に指定した場合の両方を使ってみて、

「デフォルトは“省略したときだけ効く型の初期値”なんだな」

という感覚をつかんでみてください。

タイトルとURLをコピーしました