「デフォルト引数」と「型」はセットで考えるもの
まず前提から整理します。
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 ですが、
デフォルト値 "名無しさん" があるので、
関数の中では name は string として扱えます。
もう少し厳密に書くなら、こうです。
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 を渡しても、その中のプロパティはオプション
関数の中では、debug と timeout にデフォルト値を決めてから使う
という設計になっています。
呼び出し側はこう書けます。
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この書き方だと、
debug と timeout は関数の中では常に boolean と number
呼び出し側はオプションとして渡せる
という、かなり気持ちいい状態になります。
デフォルト引数を使うときに気をつけたい落とし穴
「参照型のデフォルト値」は共有される
これは 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 を誰が処理するのか?」
「外から見たとき、この引数は“オプション”として見えていてほしいか?」
を一度立ち止まって考えてみる。
その感覚が育ってくると、
デフォルト引数は「ちょっと便利な構文」ではなく、
“関数の責任範囲をきれいに区切るための道具” に変わっていきます。
