TypeScript | 関数・クラス・ジェネリクス:関数設計の深化 – void戻り値の設計判断

TypeScript
スポンサーリンク

ゴール:「とりあえず void」から卒業する

まず一番大事なことを先に言います。

多くの初心者は、
「戻り値を使わないから、とりあえず void
と書きがちです。

でも、本当にそうしていい場面と、
void にしてしまうと、もったいない・危ない」 場面があります。

ここでは、

  • そもそも void とは何か
  • どんなときに void を選ぶのが“良い設計”なのか
  • 逆に、void を避けて「値を返すべき」なのはどんなときか

を、例を交えながら丁寧に整理していきます。


そもそも TypeScript の void って何者?

「何も返さない」ではなく「戻り値を“使わない”」という意味

まず、void を「何も返さない」とだけ覚えていると、少し足りません。

function log(message: string): void {
  console.log(message);
}
TypeScript

この関数は、戻り値を返していません。
なので void で合っています。

でも、JavaScript 的には、
戻り値がない関数は暗黙に undefined を返しています。

TypeScript の void は、
「戻り値があってもなくてもいいけど、“呼び出し側はそれを使わない”」
というニュアンスに近いです。

function f(): void {
  return undefined; // 実は OK
}
TypeScript

つまり、

  • 設計として「この関数の戻り値は意味を持たない」
  • 呼び出し側は「戻り値を見ない前提で使う」

という約束を表すのが void です。

void と undefined の違いをざっくり押さえる

function f1(): void {}
function f2(): undefined {
  return undefined;
}
TypeScript

f1f2 は、実行時の挙動はほぼ同じです。
でも、型としては意味が違います。

  • void
    「戻り値を使わない前提」
  • undefined
    「明確に undefined を返す」

void は「呼び出し側にとって“どうでもいい”」
undefined は「呼び出し側にとって“意味のある値(undefined)”」

この違いを頭の片隅に置いておくと、
「ここは本当に void でいいのか?」を考えやすくなります。


「void でいい」関数の典型パターン

副作用だけを目的とした関数

例えば、ログ出力。

function logInfo(message: string): void {
  console.log("[INFO]", message);
}
TypeScript

この関数の目的は「コンソールに出すこと」です。
呼び出し側が戻り値を使う余地はありません。

logInfo("起動しました");
// const result = logInfo("起動しました"); // result を使う意味がない
TypeScript

こういう「副作用だけが目的」の関数は、
void が素直で、読み手にも意図が伝わりやすい です。

イベントハンドラ(コールバック)として呼ばれる関数

DOM やライブラリのイベントハンドラも、基本は void で十分です。

type ClickHandler = (event: MouseEvent) => void;

const onClick: ClickHandler = (event) => {
  console.log("clicked", event.clientX, event.clientY);
};
TypeScript

イベントハンドラは「イベントが起きたときに何かする」のが目的で、
戻り値を使うことはほとんどありません。

この場合も、void は自然な選択です。

「通知」や「発火」だけをする関数

例えば、購読者に通知を送る関数。

type Listener = (message: string) => void;

class Notifier {
  private listeners: Listener[] = [];

  addListener(listener: Listener): void {
    this.listeners.push(listener);
  }

  notify(message: string): void {
    for (const l of this.listeners) {
      l(message);
    }
  }
}
TypeScript

notify の目的は「リスナーを呼ぶこと」であって、
何か値を返して呼び出し側に渡すことではありません。

こういう「発火するだけ」の関数も、void がしっくりきます。


「本当に void でいいの?」と立ち止まるべき場面

状態を変える関数:戻り値で「結果」を返した方がいいことが多い

例えば、配列に要素を追加する関数を考えます。

function addItem(items: string[], item: string): void {
  items.push(item);
}
TypeScript

これは「副作用で配列を書き換える」関数です。
void でも動きます。

でも、こう書くこともできます。

function addItemImmutable(items: string[], item: string): string[] {
  return [...items, item];
}
TypeScript

こちらは「新しい配列を返す」関数です。

どちらが良いかは設計次第ですが、
「戻り値で“新しい状態”を返す設計」の方が、テストしやすく・再利用しやすい
ことが多いです。

void にしてしまうと、

  • 呼び出し側からは「何が起きたのか」が見えにくい
  • 関数の中で何をどれだけ書き換えているか、追いかけないと分からない

という状態になりがちです。

「状態を変える関数」を書くときは、

  • 本当に副作用ベースで設計したいのか
  • それとも「新しい値を返す」形にした方がきれいか

を一度考えてみてください。

成功・失敗がありそうな処理

例えば、ユーザーを保存する関数。

function saveUser(user: User): void {
  // DB に保存する(かもしれない)
}
TypeScript

void にしてしまうと、呼び出し側は

  • 成功したのか
  • 失敗したのか
  • 何かエラー情報があるのか

を、戻り値からは一切知ることができません。

こういう処理は、たとえば次のように設計できます。

type SaveResult =
  | { ok: true }
  | { ok: false; error: string };

function saveUser(user: User): SaveResult {
  try {
    // 保存処理
    return { ok: true };
  } catch (e) {
    return { ok: false, error: String(e) };
  }
}
TypeScript

呼び出し側は、戻り値を見て分岐できます。

const result = saveUser(user);

if (!result.ok) {
  console.error("保存失敗:", result.error);
}
TypeScript

「結果に意味がある処理」を void にしてしまうと、
“意味のある情報”を捨ててしまうことになる

という感覚を持っておいてください。

チェーンして使いたくなりそうな処理

例えば、ビルダー的な API。

class QueryBuilder {
  private whereClause: string[] = [];

  where(condition: string): void {
    this.whereClause.push(condition);
  }
}
TypeScript

void にしてしまうと、こういう書き方はできません。

// builder.where("id = 1").where("deleted = 0"); // これは書けない
TypeScript

戻り値を this にすると、チェーンできます。

class QueryBuilder {
  private whereClause: string[] = [];

  where(condition: string): this {
    this.whereClause.push(condition);
    return this;
  }
}

const builder = new QueryBuilder();
builder.where("id = 1").where("deleted = 0");
TypeScript

「あとでチェーンしたくなりそうな API」を void にしてしまうと、
拡張性を自分で殺してしまうことになります。

「将来チェーンしたくなりそうか?」
も、void を選ぶ前に一度考えてほしいポイントです。


コールバックの void と「戻り値を無視する」設計

map / filter / reduce と forEach の違いを思い出す

配列メソッドで考えると分かりやすいです。

[1, 2, 3].map((n) => n * 2);      // 戻り値あり
[1, 2, 3].filter((n) => n > 1);   // 戻り値あり
[1, 2, 3].forEach((n) => console.log(n)); // 戻り値は使わない
TypeScript

map のコールバックは「戻り値が意味を持つ」ので、
型は (value: T) => U です。

forEach のコールバックは「戻り値を使わない」ので、
型は (value: T) => void です。

自分で関数を設計するときも同じです。

type Predicate<T> = (value: T) => boolean; // 戻り値に意味がある
type Effect<T> = (value: T) => void;       // 副作用だけ
TypeScript

「このコールバックは、戻り値に意味があるのか?」
「それとも、“やることをやるだけ”でいいのか?」

ここを意識して、booleanT にするか、void にするかを決めます。

「戻り値を無視する」ことを型で表現する

例えば、Promise の then

promise.then((value) => {
  console.log(value);
});
TypeScript

このコールバックの戻り値は、
次の then に渡される値として意味を持ちます。

promise
  .then((value) => value * 2)   // 戻り値が次に渡る
  .then((value) => console.log(value));
TypeScript

もし「ここでは戻り値を使わせたくない」コールバックを設計するなら、
void を使うのが自然です。

type DoneHandler = () => void;
TypeScript

「戻り値を無視する前提のコールバック」には void を付けることで、
“ここでは値を返す意味はないよ”と型で伝えられる

というのがポイントです。


実務的な判断基準をまとめておく

自分に投げるべき質問

関数の戻り値を設計するとき、
こんな質問を自分に投げてみてください。

「この関数の“結果”は、呼び出し側にとって意味があるか?」
意味がある → 何かしらの型で返す
意味がない → void でよい候補

「この関数は“状態を変える”のか、“新しい値を返す”のか?」
新しい値を返す設計にできるなら、その方がテストしやすい

「将来、この関数をチェーンして使いたくなりそうか?」
なりそう → this や自分自身を返す設計を検討

「このコールバックの戻り値は、呼び出し側で使われるべきか?」
使われる → Tboolean など
使われない → void

このあたりを一度考えてから、
「それでも戻り値に意味を持たせない」と決めたときに void を選ぶ
という順番がいいです。

「何も考えずにとりあえず void」ではなく、
「考えたうえで void を選ぶ」状態を目指してください。


まとめ:void 戻り値の設計判断を自分の言葉で言うと

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

void は、

「この関数の戻り値は、呼び出し側にとって意味を持たない」
「戻り値を使わない前提で設計されている」

という宣言。

素直に void でいいのは、

副作用だけが目的の関数(ログ、通知、イベントハンドラなど)
戻り値を使わないコールバック(forEach 的なもの)

一方で、

状態を変える処理
成功・失敗がある処理
チェーンして使いたくなりそうな API

を安易に void にしてしまうと、
「本来返せたはずの“意味のある情報”」を捨ててしまうことになる。

コードを書くとき、
「この関数の“結果”は、本当に捨てていいのか?」
と一度自分に問いかけてから、void を選んでみてください。

その一呼吸で、
void は「なんとなく付ける型」から、
“意図的に何も返さないことを示す、設計上のメッセージ” に変わっていきます。

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