TypeScript | 関数・クラス・ジェネリクス:関数設計の深化 – デフォルト引数と型

TypeScript
スポンサーリンク

「デフォルト引数」と「型」はセットで考えるもの

まず前提から整理します。

TypeScript の「デフォルト引数」は、
「引数が渡されなかったときに使う値を、関数の宣言側で決めておく仕組み」です。

function greet(name: string = "名無しさん") {
  console.log(`こんにちは、${name}`);
}

greet();            // こんにちは、名無しさん
greet("田中");      // こんにちは、田中
TypeScript

ここで大事なのは、

「デフォルト引数を使うと、その引数の型と“undefined の扱い”が変わる

という点です。

ただ「省略できて便利」ではなく、
「型の世界で何が起きているか」を理解しておくと、一気に設計が楽になります。


基本:デフォルト引数を付けると、その引数は「必須扱い」になる

? とデフォルト引数の違い

次の2つを比べてみます。

function f1(x?: number) {
  // x: number | undefined
}

function f2(x: number = 10) {
  // x: number
}
TypeScript

呼び出し側から見ると、どちらも「引数を省略できる」ように見えますが、
型の意味はまったく違います。

f1 の場合:

f1();        // OK
f1(5);       // OK
TypeScript

関数の中では x の型は number | undefined です。
つまり、「undefined かもしれない」ので、毎回チェックが必要になります。

f2 の場合:

f2();        // OK(x には 10 が入る)
f2(5);       // OK
TypeScript

関数の中では x の型は number だけ です。
「省略されたときは 10 が入る」と決まっているので、
undefined を気にする必要がありません。

ここが超重要ポイントです。

デフォルト引数を使うと、

「呼び出し側からはオプションっぽく見えるのに、
関数の中では“必ず値がある”前提で書ける」

という状態を作れます。

つまり、
「外からはオプション、内側では必須」
という、かなりおいしい設計ができるわけです。


デフォルト引数と型推論:型を書かなくても、デフォルト値から決まる

デフォルト値が型のヒントになる

次のように書いたとします。

function greet(name = "名無しさん") {
  console.log(`こんにちは、${name}`);
}
TypeScript

型を書いていませんが、TypeScript はこう推論します。

function greet(name?: string): void;
TypeScript

正確には、引数 name の型は string | undefined ですが、
デフォルト値 "名無しさん" があるので、
関数の中では namestring として扱えます。

もう少し厳密に書くなら、こうです。

function greet(name: string = "名無しさん") {
  console.log(`こんにちは、${name}`);
}
TypeScript

この場合、name の型は完全に string です。

ポイントは、

「デフォルト値の型が、その引数の型の“最低ライン”になる」

ということです。

例えば、こう書くとエラーになります。

function f(x: number = "10") {
  // エラー:string を number に代入できない
}
TypeScript

デフォルト値 "10" が string なので、
x: number とは矛盾してしまうからです。

つまり、
「デフォルト値は、その引数の型と矛盾してはいけない」
というルールが自然に効いてきます。


オプション引数+デフォルト値の組み合わせはどうなるか

x?: number と x: number = 10 の違いをもう一歩深掘り

次の2つをもう一度見てみます。

function f1(x?: number) {
  // x: number | undefined
}

function f2(x: number = 10) {
  // x: number
}
TypeScript

どちらも「引数を省略できる」点は同じですが、
設計としての意味はこう変わります。

f1 の設計:

「呼び出し側は、x を渡してもいいし、省略してもいい。
その代わり、関数の中では“x がないかもしれない”世界と戦う。」

f2 の設計:

「呼び出し側は、x を渡してもいいし、省略してもいい。
省略されたときは、関数側が“10 を入れておく”と決めている。
だから、関数の中では“x は必ず number”として扱える。」

つまり、
「undefined を処理する責任を、呼び出し側に残すか、関数側で引き受けるか」
の違いです。

デフォルト引数を使うというのは、
「undefined の処理を関数の入り口で全部引き受ける」という宣言でもあります。


デフォルト引数とオブジェクト引数の組み合わせ

オプションが多いときは「オプションオブジェクト+デフォルト」で設計する

例えば、設定を受け取る関数を考えます。

type Options = {
  debug?: boolean;
  timeout?: number;
};

function runTask(options: Options = {}) {
  const debug = options.debug ?? false;
  const timeout = options.timeout ?? 3000;

  console.log("debug:", debug);
  console.log("timeout:", timeout);
}
TypeScript

ここでは、

呼び出し側は options 自体を省略できる
options を渡しても、その中のプロパティはオプション
関数の中では、debugtimeout にデフォルト値を決めてから使う

という設計になっています。

呼び出し側はこう書けます。

runTask();                                  // すべてデフォルト
runTask({ debug: true });                   // debug だけ指定
runTask({ timeout: 1000 });                 // timeout だけ指定
runTask({ debug: true, timeout: 5000 });    // 両方指定
TypeScript

ここでのポイントは、

「オプションが複数あるときは、
位置引数ではなく“オプションオブジェクト+デフォルト値”で設計すると、
型もコードもきれいになる」

ということです。

さらに一歩進めると、引数のところで分割代入+デフォルトも使えます。

type Options = {
  debug?: boolean;
  timeout?: number;
};

function runTask({ debug = false, timeout = 3000 }: Options = {}) {
  console.log("debug:", debug);
  console.log("timeout:", timeout);
}
TypeScript

この書き方だと、

debugtimeout は関数の中では常に booleannumber
呼び出し側はオプションとして渡せる

という、かなり気持ちいい状態になります。


デフォルト引数を使うときに気をつけたい落とし穴

「参照型のデフォルト値」は共有される

これは JavaScript の話でもありますが、
TypeScript でも同じように注意が必要です。

function addItem(item: string, list: string[] = []) {
  list.push(item);
  return list;
}

const a = addItem("A");
const b = addItem("B");

console.log(a); // ["A"]
console.log(b); // ["B"] だと思う?
TypeScript

実は、ここは毎回新しい配列が作られているので問題ありません。
(デフォルト値の [] は、呼び出しごとに評価されます)

ただし、クラスのプロパティ初期化など、
「1回だけ評価されて共有される」ケースもあるので、
「参照型のデフォルト値」は文脈によっては注意が必要です。

関数のデフォルト引数に関しては、
基本的には「毎回評価される」ので、
配列やオブジェクトをデフォルトにしても大丈夫です。

function createUser(name: string, tags: string[] = []) {
  return { name, tags };
}
TypeScript

このようなケースでは、
「デフォルト値としての配列・オブジェクトは、呼び出しごとに新しく作られる」
と理解しておけばOKです。


どんなときに「デフォルト引数+型」が特に効くか

「外からはオプション、内側では必須」にしたいとき

例えば、ログ出力関数。

function log(message: string, level: "info" | "warn" | "error" = "info") {
  console.log(`[${level}] ${message}`);
}
TypeScript

呼び出し側はこうです。

log("起動しました");                 // level は "info"
log("警告です", "warn");
log("エラーです", "error");
TypeScript

ここでは、

呼び出し側にとっては level はオプション
関数の中では level は必ず "info" | "warn" | "error" のどれか

という設計になっています。

もしこれをオプション引数だけで書くと、こうなります。

function log(message: string, level?: "info" | "warn" | "error") {
  const safeLevel = level ?? "info";
  console.log(`[${safeLevel}] ${message}`);
}
TypeScript

これでもいいですが、
「毎回 safeLevel を作る」ひと手間が増えます。

デフォルト引数を使うと、
「関数の入り口で“必ずある形”にしてしまう」
という設計が、より自然に書けるようになります。


まとめ:「デフォルト引数と型」を自分の言葉で言うと

最後に、あなた自身の言葉でこう整理してみてください。

デフォルト引数は、

「呼び出し側からはその引数を省略できるようにしつつ、
関数の中では“必ず値がある”前提で書けるようにする仕組み」

x?: T は「省略できるし、中では T | undefined」
x: T = defaultValue は「省略できるけど、中では常に T」

オプションが増えたら、
「オプションオブジェクト+デフォルト値」で設計すると、
型もコードもきれいになる。

そして、デフォルト引数を使うときは、
「undefined を誰が処理するのか?」
「外から見たとき、この引数は“オプション”として見えていてほしいか?」
を一度立ち止まって考えてみる。

その感覚が育ってくると、
デフォルト引数は「ちょっと便利な構文」ではなく、
“関数の責任範囲をきれいに区切るための道具” に変わっていきます。

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