37
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

TypeScriptで戻り値がvoid型の関数を扱う場合の注意点

Last updated at Posted at 2023-02-06

この記事は、TypeScriptで戻り値がvoid型の関数を扱う場合の注意点に関する覚書です。

TL;DR

  • TypeScript(JavaScript)における明示的な戻り値を持たない関数は、ランタイムではundefinedを返す。
  • TypeScriptにおける関数の戻り値としてのvoid型undefined型よりも「戻り値の利用を想定していないことを明示できる」点で優位性があるが、知らないとハマりそうなassignabilityの罠がある。
  • 上記のassignabilityの仕様を把握しておきつつ、以下のように型注釈することがbetterだと思っている。
型注釈をつける関数の種類
(戻り値の利用想定)
戻り値の型注釈 関数型の使用
1. 副作用だけの関数
(利用なし)
⭕️ void
(🔺 undefined
極力避けるべき
(理由)
2. 戻り値のないreturn文で
early returnする関数
(利用あり)
⭕️ T | void
(🔺 T | undefined
OK

※ 前提として、私はReturn Typeは積極的に書きたい派です。

他言語の関数における戻り値の扱い

いきなり他言語に脱線するのですが、 私が学生の頃に書いたことがあるFortranでは「呼び出されると入力に対し何らかの処理を実行するサブプログラム」のうち、関数(function)は値を返すもの、サブルーチン(subroutine)は値を返さないもの、といった分類があります。

TypeScriptにおける戻り値の扱い

一方で、TypeScriptでは実装として明示的な戻り値を持たない関数はvoid型の戻り値を持つものとして推論されます。
また、TypeScript(JavaScript)において、これらの関数はランタイムにundefinedを返します。すなわち実行時に戻り値を返さない関数は存在しません。サンプルコードで見てみましょう。

// 暗黙的にundefinedを返す関数の戻り値はvoid型と推論される
const f1 = () => {}; // () => void
const f2 = () => {return}; // () => void
console.log(undefined === f1()); // true
console.log(undefined === f2()); // true

// 明示的にundefinedを返す関数の戻り値はundefined型と推論される
const f3 = () => {return undefined}; // () => undefined
console.log(undefined === f3()); // true

void型について

void型は関数の返り値の型として使われることを想定しており、「なにも返さない」ことを表す型です。
公式ドキュメントにはvoid型についてa subtype of undefined intended for use as a return type.と記載されており、戻り値の型として使用されることが想定されていることがわかります。

※ 余談ですが2023/2/1現在では上記の記載がありますが、厳密にはvoid型はundefined型のsupertypeであり、上記は誤記であると思われます。またこれに対するPull Requestも作成されています。

また、下記サイトから一部引用します。

TypeScriptの型上の意味としては、undefined型とvoid型は同じです。したがって、戻り値の型注釈にundefinedを用いることもできます。

ふむ、「undefined型とvoid型は同じ」とあります。

ただし、(個人的な感覚として)それでも暗黙的にundefindedを返す関数には積極的にvoid型で型付けしたいと考えています。なぜなら、明示的に戻り値を返す関数と暗黙的にundefinedを返す関数は、戻り値の利用を想定しているかの点で本質的には異なるものだと考えているからです。
よって、これ以降では暗黙的にundefinedを返す関数の戻り値の型注釈には「なにも返さない(戻り値が利用されることを想定していない)」ことを明示するvoid型を極力利用するという方針のもと、ここからは型注釈をつける関数の種類ごとにみていきます。

1. 副作用だけの関数

1.1 関数型を使わない場合

これは以下のようなケースです。

// OK
const f1 = (): void => {console.log('TS is wakaran')};

// NG
// エラー: A function whose declared type is neither 'void' nor 'any' must return a value.(2355)
const f2 = (): undefined => {console.log('TS is chittomo wakaran')};

// OK
const f3 = (): undefined => {
  console.log('TS is completely understood');
  return;
};

f2では「voidかanyでない戻り値の宣言がある関数はreturn文が必須」だとコンパイラに指摘されています。解消するためはf3のようにすれば良いですが、undefined型で型注釈することによってreturn文を書く必要が発生するのは本末転倒な気がします(コンパイル後のコードにももちろん残ります)。よって、f1のように素直にvoid型で注釈するのが良いでしょう。

1.2 関数型を使う場合

ここで、一点注意点として、このような関数に関数型を用いて型付けをする場合には、いささか慎重に取り扱う必要があります。それは戻り値がvoid型の関数型で型付けした関数は、関数宣言時点で戻り値の方に関わらずType Errorが発生しないためです。ただし、そのような関数の戻り値の型はvoid型で注釈されているため、戻り値に対して何らかの演算をかけたタイミングでType Errorとなりえます。以下のコードでみてみましょう。

type F = () => void;
// いずれもコンパイルは通る
const f1: F = () => {console.log('TS is yappa wakaran')};
const f2: F = () => {return 100};
const f3: F = () => {return {foo: 100}};

const r3 = f3();
// エラー: Property 'foo' does not exist on type 'void'.(2339)
r3.foo++;

上記のような単純な例であれば特段問題にならないかもしれませんが、関数の利用者が安全にそれ呼び出すためにも、関数実装時点でこのような型不正は防がれるべきです。よって、戻り値がvoid型の関数に型注釈をする場合にはそもそも関数型を使用しないほうが安全だと思います。また同様の理由から、Call Signaturesとして() => voidなどで型注釈する際にも注意が必要です。

2. 戻り値のないreturn文でearly returnする関数

2.1 関数型を使わない場合

前提として、下記のようなearly return(早期リターン)を含む関数は、undefined型とのUnion Typesとして型推論されます(void型じゃないんだ☺️)冒頭の結果もあって、この記事の内容を整理するまではvoid型とのUnion Typesと推論されるのかと思っていましたが、関数全体として戻り値が利用される可能性がないケースしかvoid型は推論結果に登場しないように思われます。

// 関数の型推論: () => "lucky!" | undefined
const judgeFortune = () => {
  if (Math.random() > 0.99) return;
  return 'lucky!';
};

さて、とはいえこのようなケースでも、型注釈をつけるなら私はvoid型とのUnion Typesで型付けします。それは以下の理由からです。

  1. early reurnの対象となるケース(条件)においてもundefinedという実行結果を呼び出し元で利用することを想定しているわけではない。
  2. Union Typesであれば型チェックでエラーにならない(下記のコードブロックを参照)。

2.2 関数型を使う場合

また、上記のような戻り値がvoid型とのUnion Typesになるようなケースでは、関数型を用いて型付けしても問題ありません。最後に下記のサンプルコードを見てみましょう。

type F = () => 'bravo!' | void;

// OK
const judgeFortune: F = () => {
  if (Math.random() > 0.99) return;
  if (Math.random() > 0.99) return undefined;
  return 'bravo!';
};

// NG
const occurMisfortune: F = () => {
  /**
   * エラー: 
   * Type '() => "bravo!" | null' is not assignable to type 'F'.
   * Type '"bravo!" | null' is not assignable to type 'void | "bravo!"'.
   * Type 'null' is not assignable to type 'void | "bravo!"'.(2322)
   */
  if (Math.random() > 0.99) return null;
  return 'bravo!';
}

上記ではvoid型とのUnion Typesの戻り値をもつ関数型のFで関数に型注釈をつけていますが、関数実装時点でvoid型のsubtypeでないnullはType Errorとして検出できています。

しめくくり

今回TypeScriptのvoid型について再整理するにあたり、これまで自分の中でもあまり意識していなかったいくつかの項目に気づくことができました。また、私にとっては初めての記事作成となりましたが、web業界のエンジニアとなって1年が経過したこともあり、自身の知見の整理のためにも大小問わずこれからも書いていけたらと思います。本稿に対し、ご指摘やご意見などございましたら、いつでもコメントいただけると嬉しいです🙋

参考にさせていただいたもの

37
24
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?