95
70

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 〜TypeScript始めたてのあの頃に知っておきたかったこと〜Advent Calendar 2021

Day 14

【TypeScript】結局のところfunctionとアロー関数はどちらを使うべきなのか?

Last updated at Posted at 2021-12-14

アロー関数がただの代替構文ではないということは皆さまご存じかと思いますが、ではどちらをどのタイミングで使うべきなのかという論点については人によって結論がまちまちというか、まあ好みでいいじゃんみたいにはぐらかされることが多いように感じます。

ですがチーム開発ではそんなことも言っていられない1ので、今回は両者の性質の違いからstrictモード + TypeScriptの環境においてどちらをいつ使うのが合理的であるのかを考えていきます。

結論

結論から述べますと、アロー関数は

functionを置き換えるためだけに使い
functionを置き換えるために使うのは控えるべき

です。つまり以下のような関数を考えた場合

function double(...nums: number[]): number[] {
  return nums.map(function (num) {
    return num * 2
  })
}

このように書くことが望ましいです。

good
function double(...nums: number[]): number[] {
  return nums.map((num) => num * 2)
}
bad
const double = (...nums: number[]): number[] => {
  return nums.map((num) => num * 2)
}

下もネット記事などではよく見る形ですが、私も熟考の末この結論に至りました。
何故function文はアロー関数で置き換えるべきでないのか、ここからその根拠を示していきます。

単純機能比較

その前に両者の非strictモード + JavaScriptにおける機能の比較が以下の記事に詳しいので、まずはこちらの結論項にある表をご確認ください。

JavaScript: 通常の関数とアロー関数の違いは「書き方だけ」ではない。異なる性質が10個ほどある。

このうち

  • arguments
  • 引数名の重複
  • 再定義

についてはstrictモード + TypeScriptによって禁止されるため、この環境において両者の違いは

  • thisの指すもの
  • newの使用可否
  • prototypeプロパティの有無
  • yieldの使用可否(ジェネレータ)
  • superの使用可否
  • (巻き上げの有無)

の6点になります。

理由①: 安定性に関する優位性が消えた

上記の通りアロー関数で関数を定義することによって引数名の重複や同じ名前での再定義といったポカを弾くことができましたが、設定によってfunction文を用いてもキチンとエラーが出るようになりました。

理由②: 使用できない場面が多い

良いコーディング規約とは条件分岐が少ないことだと私は考えています。
つまり「この時はこっちを使っていいけど、こういう時はあっちを使おうね」みたいなやつは極力減らした方がいいということです。

仮に『関数の宣言はアロー関数を用いること』という規約を設けたとすると、
『ただし

  • コンストラクタを書く場合
  • ジェネレータ関数を書く場合
  • サブクラスでオーバーライドする際にsuperを呼ぶ必要がある場合(参考: TypeScript Deep Dive 日本語版)

にはアロー関数を使わないように』という面倒な補記を入れる必要があります。
これは書く方も読む方も面倒です。

理由③: this対策として致命的な欠陥がある

それでも関数の定義にアロー関数を使いたい理由はやはりthisかと思います。
thisの挙動を表す例としてMicrosoft謹製のwikiからコードを拝借しました。

class Hoge {
  constructor(public x: number) {}

  print(): void {
    console.log("x is " + this?.x)
  }
}

const hoge = new Hoge(3)
hoge.print() // x is 3

const fuga = { x: 10, print: hoge.print }
fuga.print() // x is 10

const piyo = fuga.print
piyo() // x is undefined

まあ確かにキモいですけど。でも正直こんなコードは書かないですよね?
普通にTypeScriptを書いていて遭遇するthisの壊れパターンなんて実際には1つしかないはずです。
それがこれ↓

const h = new Hoge(5)
setTimeout(h.print, 1000)

メソッドを渡して他の誰かに実行してもらうパターン
上記のサンプルは1秒後にx is undefined(strictモードでない場合はグローバルオブジェクト)が表示されます。

大抵の方はこれを防ぐためにprintをアロー関数で定義してthisをバインドしているかと思いますが、それって言い換えれば「メソッドがアロー関数で定義されていればメソッドの参照を渡してもいいよ」ということでつまり関数のユーザーが関数の実装を知らなければならないということになると思うんですよね。設計として致命的にヤバい。

以下の書き方であればprintが普通に定義されているかアロー関数で定義されているかに依らずthisをバインドすることができます。

setTimeout(h.print.bind(h), 1000)
setTimeout(() => h.print(), 1000)

直感的なコードは前者ですが、bindの第一引数は型安全でないというそれはそれで激ヤバな特性があるので気にされる方は後者を使うことをおすすめします。

総論

上記のような思考を踏まえて

  • 関数やメソッドの定義にはfunctionおよび通常の記法を用いる
  • 無名関数はfunctionを用いずアロー関数式で書く

としました。理由としてはやはり③が一番大きいですね。そこそこ断定的な書き方をしましたがgoogleのコーディング規約にも似たようなことが書いてあるのでそんなに間違ってはないんじゃないかなと思います。

12/21 追記

いただいたコメントが大変ためになるのでと私の[返信]
(https://qiita.com/nicco_mirai/items/0bd0d0bcee72497d4b42#comment-43528ad7c51e09646e4d)と併せてぜひご確認ください。

  1. 個人開発の場合は好みでいいと思います。

95
70
9

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
95
70

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?