🌏

なぜ TypeScript には void と undefined が存在するのか?

2023/06/17に公開

void について

値を返さない関数の戻り値は void と表現されます。

const voidFunc = (): void => {
  return;
}

voidFunc の型は以下のようになります。

const voidFunc: () => void

voidFunc は undefined を返す

voidFunc は実体として何を返しているか、というと undefined を返しています。
result には undefined が入っています。

const voidFunc = (): void => {}
const result = voidFunc();
console.log(result === undefined);
// true

void は undefined のエイリアスと勘違いしがち

voidFunc を少しだけ修正して、undefinedFunc を作成してみます。
undefinedFunc の戻り値の型は undefined です。undefinedFunc が実体として何を返しているかというと undefined を返しています。

const undefinedFunc = (): undefined => {}
const result = undefinedFunc();
console.log(result === undefined);
// true

voidFunc と undefinedFunc は戻り値の型定義に差はありますが、返している値は同じ undefined です。この挙動こそが、void って何だっけ? undefined のエイリアスなんじゃない?と勘違いする理由の一つだと思います。

void の意味

ただ、void は undefined のエイリアスではありません。
void は関数の戻り値を監視しない場合に使用する型です。
forEach を例に考えてみます。

念のために...forEach とは以下のように配列の各要素に対して、引数として受け取った関数を一度ずつ実行するメソッドです。

const array1 = ['a', 'b', 'c'];
array1.forEach(element => console.log(element));
// Expected output: "a"
// Expected output: "b"
// Expected output: "c"

forEach の型定義は以下になっており、戻り値は void になってます。

forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;

forEach を使って、target 配列に 1,2,3 を入れたい時、以下のような処理をします。

const target: number[] = [];
[1, 2, 3].forEach((element) => {
  return target.push(element);
});
console.log(target);
// [1, 2, 3]

この例を見てもわかるように、forEach は配列の各要素に引数として受け取った関数を一度ずつ実行するだけなので、戻り値は必要ないです。forEach が何を返そうと、それはどうでもいいため、戻り値の型は void として表現されています。

void の実用的な例

void は関数の戻り値を監視しない場合に使用する型であり、これはけっこう実用的です。
例えば、戻り値を返す必要がない sample という関数を作ったとします。引数の name に値が入っていれば、ログが表示されます。

const sample = (name: string | undefined): void => {
  if (name) {
    console.log(`hello ${name}`)
  }
}

sample("Beal")
// "hello Beal" 

sample 関数を作ったもの...翌月にログを表示したかどうかを戻り値として返す、という以下の修正をしたとします。

const sample = (name: string | undefined): boolean => {
  if (name) {
    console.log(`hello ${name}`)
    return true
  }
  return false
}
const result = sample("Beal")
// "hello Beal" 

さて、ここからが本題ですが、修正はブレイキングチェンジになるでしょうか??
ガッツリ、sample 関数の戻り値が変わっているため、ブレイキングチェンジになりそうですが、元々の sample 関数の戻り値は void です。
そのため、sample 関数の利用ユーザーは sample 関数の戻り値を監視していないはずです。となると、ブレイキングチェンジではない、という判断ができます。

void は undefined のエイリアスではない

先の例をもう一度振り返ります。戻り値を以下のように undefined にした上で戻り値が変わる修正をした場合は、ブレイキングチェンジになり得ます。

const sample = (name: string | undefined): undefined => {
  if (name) {
    console.log(`hello ${name}`)
  }
}

sample("Beal")
// "hello Beal" 

戻り値を undefined としていた場合は、sample の利用ユーザーが戻り値を監視している可能性を考える必要があるためです。
これが void は undefined のエイリアスではない、という発言の意図でした。

まとめ

なぜ TypeScript には void と undefined が存在するのか、という問いの答えは出たかと思います。ただ、まぁ、void の意味理解して使ってるやつどれほどおんねんって話もある😎

(おまけ) void は必ずしも undefined を返すわけではない

forEach の例をもう一度振り返ります(以下、再掲)。

const target: number[] = [];
[1, 2, 3].forEach((element) => {
  return target.push(element);
});
console.log(target);
// [1, 2, 3]

先ほどの forEach に引数として渡していた関数を pushArrayElement という関数として外出ししてみます。以下のようになりますが、もちろん、型エラーは発生しません。

const target: number[] = [];
// 関数として外出しする
const pushArrayElement = (element: number) => {
  return target.push(element);
}
[1, 2, 3].forEach((element) => pushArrayElement(element));
console.log(target);

またまた再掲ですが、forEach の型定義は以下のようになっており、callbackfn の戻り値は void として表現されています。

forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;

callbackfn として pushArrayElement を渡していますが、pushArrayElement は undefined を返してるでしょうか?

[1, 2, 3].forEach((element) => pushArrayElement(element));

pushArrayElement は Array.prototype.push() を戻り値としており、Array.prototype.push() は push 後の配列の要素数を返します。以下を見るとわかるように Array.prototype.push() の戻り値の型は number 型です。

const target: number[] = [];
const result = pushArrayElement(1)
console.log(result)
// 1

つまり、戻り値の型は void である callbackfn ですが、 undefined を返しているわけではありません。そして、それに対して型エラーも出ていません。

戻り値の型が void となっている関数は、必ずしも undefined を返すわけではなく、何を返してくるかは分かりません。void となっている時点で、戻り値を利用することはないため、型エラーの対象外になっているのだと、私は解釈しています。

参考

https://stackoverflow.com/questions/58885485/why-does-typescript-have-both-void-and-undefined

https://www.typescriptlang.org/docs/handbook/2/functions.html#assignability-of-functions

https://github.com/Microsoft/TypeScript/wiki/FAQ#why-are-functions-returning-non-void-assignable-to-function-returning-void

https://github.com/microsoft/TypeScript/issues/36239#issuecomment-575722576

Discussion