TypeScript | 関数・クラス・ジェネリクス:関数設計の深化 – 関数のオプション引数設計

TypeScript
スポンサーリンク

まず「オプション引数って何者か」を整理する

TypeScript で「オプション引数」と言うとき、
だいたい次の2つの書き方を指します。

function greet(name: string, title?: string) {
  // title はあってもなくてもいい
}

function greet2(name: string, title: string | undefined) {
  // title に undefined が来る可能性を自分で扱う
}
TypeScript

title?: 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
TypeScript

title?: 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 だけ");
TypeScript

undefined を挟んだり、
「第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」
}
TypeScript

f1 はこう呼べます。

f1();          // OK(省略)
f1("hello");   // OK
TypeScript

f2 はこうです。

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 を渡す」を区別する

コードを書くとき、
「この引数、本当に“なくてもいい”のか? なくてもいいなら、そのときどう振る舞う?」
と一度自分に問いかけてから ? を付けてみてください。

その一呼吸が、
オプション引数を「便利な逃げ道」ではなく、
設計として気持ちいい選択肢 に変えてくれます。

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