まず「オプション引数って何者か」を整理する
TypeScript で「オプション引数」と言うとき、
だいたい次の2つの書き方を指します。
function greet(name: string, title?: string) {
// title はあってもなくてもいい
}
function greet2(name: string, title: string | undefined) {
// title に undefined が来る可能性を自分で扱う
}
TypeScripttitle?: string は「呼び出し側が省略できる引数」、string | undefined は「値として undefined が来る可能性がある引数」です。
見た目は似ていますが、
「誰が undefined を処理するのか」「どこまでを関数の責任にするのか」が変わります。
ここからは、
オプション引数を「どう設計するか」という視点で、
初心者がつまずきやすいポイントを丁寧にほどいていきます。
基本:オプション引数は「呼び出し側の負担を減らすための仕組み」
? が付くと「その引数を省略して呼べる」
一番シンプルな例から。
function greet(name: string, title?: string) {
if (title) {
console.log(`${title} ${name} さん、こんにちは`);
} else {
console.log(`${name} さん、こんにちは`);
}
}
greet("田中"); // OK(title 省略)
greet("田中", "社長"); // OK
TypeScripttitle?: string と書くと、
呼び出し側は「第2引数を渡してもいいし、渡さなくてもいい」状態になります。
関数の中では、title の型は string | undefined です。
つまり、「オプション引数は、関数の中では“undefined かもしれない値”として扱う必要がある」 ということです。
ここでのポイントは、
呼び出し側にとっては「楽になる」
代わりに、関数の中で「undefined をちゃんと扱う責任」が生まれる
というバランスです。
設計ポイント1:「本当にオプションにしたいのか?」を一度疑う
何でもかんでも ? を付けると、関数の中がつらくなる
初心者がやりがちなのが、「迷ったらとりあえずオプションにしておく」パターンです。
function createUser(name?: string, age?: number) {
// ここから先、全部「あるかもしれない」「ないかもしれない」と戦うことになる
}
TypeScriptこうすると、関数の中で毎回
if (!name) { ... }
if (age === undefined) { ... }
TypeScriptといったチェックが必要になり、
ロジックがどんどん複雑になります。
オプション引数を設計するときに、まず自分に聞いてほしいのはこれです。
「この引数は、本当に“なくてもいい”のか?」
「なくてもいいなら、そのときの振る舞いをちゃんと決めているか?」
例えば、name は本当に必須なら、こうすべきです。
function createUser(name: string, age?: number) {
// name は必須、age はあってもなくてもいい
}
TypeScript「何となく怖いから全部オプション」ではなく、
“この関数が成立するために絶対必要なもの”と“あってもなくてもいいもの”を分ける。
これがオプション引数設計の一番最初のルールです。
設計ポイント2:オプション引数が増えたら「オブジェクト1個」にまとめる
位置引数のオプションが増えると、呼び出し側が地獄になる
例えば、こんな関数を考えます。
function sendMail(
to: string,
subject?: string,
body?: string,
cc?: string,
bcc?: string
) {
// ...
}
TypeScript呼び出すときに、こうなります。
sendMail("a@example.com"); // subject, body, cc, bcc すべて省略
sendMail("a@example.com", "件名だけ");
sendMail("a@example.com", undefined, "本文だけ");
sendMail("a@example.com", "件名", "本文", undefined, "bcc だけ");
TypeScriptundefined を挟んだり、
「第3引数が何だったか」を覚えておく必要があったり、
呼び出し側の負担が一気に増えます。
こういうときは、
「オプションが増えてきたら、オブジェクト1個にまとめる」 のが鉄板です。
type SendMailOptions = {
subject?: string;
body?: string;
cc?: string;
bcc?: string;
};
function sendMail(to: string, options: SendMailOptions = {}) {
const { subject, body, cc, bcc } = options;
// ...
}
TypeScript呼び出し側はこう書けます。
sendMail("a@example.com");
sendMail("a@example.com", { subject: "件名だけ" });
sendMail("a@example.com", { body: "本文だけ" });
sendMail("a@example.com", { subject: "件名", bcc: "bcc@example.com" });
TypeScriptこれなら、
どのオプションを指定しているかが一目で分かる
順番を気にしなくていい
不要なところに undefined を書かなくていい
というメリットがあります。
「オプション引数が2つを超えたら、オブジェクトにまとめるか検討する」
くらいの感覚を持っておくと、設計がかなり楽になります。
設計ポイント3:オプション引数には「デフォルト値」を積極的に使う
関数の中で「undefined を潰してから」使う
オプション引数は、関数の中では undefined かもしれません。
毎回 if でチェックするのは面倒なので、
「最初にデフォルト値を決めてしまう」 のが賢いやり方です。
function greet(name: string, title?: string) {
const safeTitle = title ?? "さん";
console.log(`${name} ${safeTitle}、こんにちは`);
}
TypeScriptあるいは、引数のところで直接デフォルト値を指定することもできます。
function greet(name: string, title: string = "さん") {
console.log(`${name} ${title}、こんにちは`);
}
TypeScriptこの書き方だと、title は「呼び出し側から見るとオプション」ですが、
関数の中では常に string として扱えます。
greet("田中"); // title は "さん"
greet("田中", "社長"); // title は "社長"
TypeScriptここでのポイントは、
「オプション引数をそのまま string | undefined として引きずらない」
「関数の入り口で“必ずある形”に変換してしまう」
という設計です。
「オプションで受ける」ことと「中で optional のまま扱う」ことは別問題 だと意識しておくと、コードがかなりスッキリします。
設計ポイント4:オプション引数と「オーバーロード」の使い分け
「引数のパターンごとに意味が違う」なら、オーバーロードを検討する
例えば、こういう関数を考えます。
function readFile(path: string, encoding?: string) {
// encoding があれば文字列、なければバイナリを返したい…とする
}
TypeScript本当は、
encoding が指定されているとき → string を返す
encoding がないとき → Buffer(バイナリ)を返す
という仕様にしたいとします。
オプション引数だけで書くと、戻り値の型が曖昧になります。
function readFile(path: string, encoding?: string): string | Buffer {
// 呼び出し側からは「毎回 union をさばく」必要が出てしまう
}
TypeScriptこういうときは、
「オプション引数」ではなく「オーバーロード」でパターンを分ける」 方がきれいです。
function readFile(path: string): Buffer;
function readFile(path: string, encoding: string): string;
function readFile(path: string, encoding?: string): string | Buffer {
if (encoding) {
// 文字列として読む
return "file content";
} else {
// バイナリとして読む
return Buffer.from([]);
}
}
TypeScript呼び出し側はこうなります。
const bin = readFile("a.txt"); // bin: Buffer
const text = readFile("a.txt", "utf-8"); // text: string
TypeScript「引数があるかないかで、戻り値の型まで変わる」ような関数は、
オプション引数だけで表現しようとすると、
呼び出し側が毎回 union と戦うことになります。
そういうときは、
「オプション引数」ではなく「別パターンの関数」として型レベルで分ける
という選択肢を持っておくと、設計の幅が広がります。
設計ポイント5:「undefined を渡す」のか「省略する」のかを意識する
? と | undefined の違いをちゃんと使い分ける
次の2つは、似ているようで意味が違います。
function f1(x?: string) {
// 呼び出し側は「引数を省略できる」
}
function f2(x: string | undefined) {
// 呼び出し側は「必ず引数を渡すが、その値として undefined もOK」
}
TypeScriptf1 はこう呼べます。
f1(); // OK(省略)
f1("hello"); // OK
TypeScriptf2 はこうです。
f2(undefined); // OK(渡している)
f2("hello"); // OK
// f2(); // エラー:引数が足りない
TypeScript設計としては、
「その引数は“本当に省略していい”のか?」
「それとも、“必ず渡してほしいけど、その中身が undefined のことはある”のか?」
を分けて考える必要があります。
例えば、「設定オブジェクト」の中のプロパティなら、| undefined よりも ? の方が自然なことが多いです。
type Options = {
debug?: boolean; // 指定しなければ「デフォルトの挙動」
};
TypeScript一方で、「この関数は必ず options を受け取るが、その中の値は undefined かもしれない」
という設計なら、こうも書けます。
type Options = {
debug: boolean | undefined;
};
function run(options: Options) {
// options は必ず渡される
}
TypeScript「省略できる」のか、「必ず渡すが中身が undefined かもしれない」のか。
オプション引数設計では、この2つを意識的に使い分けることが大事です。
まとめ:「関数のオプション引数設計」を自分の言葉で言うと
最後に、あなた自身の言葉でこう整理してみてください。
オプション引数は、
「呼び出し側がその引数を省略できるようにする仕組み」
その代わり、関数の中では undefined をちゃんと扱う責任が生まれる。
だから、
本当に“なくても成立する”ものだけをオプションにする
オプションが増えてきたら、位置引数ではなくオブジェクトにまとめる
関数の入り口でデフォルト値を決めて、optional のまま引きずらない
引数の有無で戻り値の型まで変わるなら、オーバーロードも検討する? と | undefined の違いを意識して、「省略」と「undefined を渡す」を区別する
コードを書くとき、
「この引数、本当に“なくてもいい”のか? なくてもいいなら、そのときどう振る舞う?」
と一度自分に問いかけてから ? を付けてみてください。
その一呼吸が、
オプション引数を「便利な逃げ道」ではなく、
設計として気持ちいい選択肢 に変えてくれます。
