Edited at

TypeScriptのOptional Chainingは用法用量を守って正しく使え


TypeScriptの3.7.2

TypeScriptの新バージョンが11月にリリースされました。

Announcing TypeScript 3.7 | TypeScript

私自身、特に注目していたのが Optional Chaining でした。

簡単に説明をすると下記のように書くことができます。

let x = foo?.bar.baz();

今までであればこんな書き方としていたものや

let x = (foo === null || foo === undefined) ?

undefined :
foo.bar.baz();

他にもこんな書き方をしていたものを簡潔に書くことができて、とても人に優しい書き方ができますね。

let x = foo && foo.bar.baz()

実際に私も携わっているプロジェクトで foo && foo.bar && foo.bar.hoge というようにしなければいけないことが多く、この見にくいコードから開放されれば良いと 3.7 のリリースをまだかまだかと思っていました。そういった開発者の方は多いのではないかなと思います。

今回はReactを使ったプロジェクトのTypeScriptのバージョンをあげ環境を整える部分と、実際に使ってみた感想と注意点をご紹介できればと思います。


始める際の留意点

大体の場合下記の手順でできるはずです。


  1. tsのバージョンを上げる

  2. @babel/plugin-proposal-optional-chainingを導入

  3. VSCodeのワークスペースのTSバージョンを上げる

  4. prettierを最新バージョンにする

1と2はいいとして3、4は私もちょっとハマったとこなのでピックアップして説明しますね。すでに環境が整っている方はここは飛ばしてしまって良いです。


VSCodeのワークスペースのTSバージョンを上げる

VS Codeを使っている場合VS CodeのワークスペースのTSバージョンを変更しないとエラーになってしまいます。

image.png

エラーを解消するにはVS Codeの右下の方にある数字の部分をクリック(ここだと3.6.3)して 3.7.2 を選びます。

image.png


prettierを最新バージョンにする

2019/11/08 現在だとnpmにprettierの最新バージョンが上がっていません。が、githubのmasterでは 3.7.2 に対応しているのでそちらを使うようにします。

"prettier": "github:prettier/prettier#master"

ここまでやれば実際に Optional Chaining を使う環境ができたと思います。

2019/11/11追記

prettier v1.9 でOptional Chainingのサポートがされたようです。

https://github.com/prettier/prettier/blob/master/CHANGELOG.md#1191


トランスパイルするとどうなんの?

使うことができたんですが、トランスパイルされたコードはどうなっているのでしょう?トランスパイルされたコードも確認してみましょう。

TypeScript Playground でコードを書きながら試すことができます。

例えばこんなコードがあるとして

interface Foo {

name: string
bar: {
name: string
baz?: {
name: string
}
}
}

let foo: Foo

foo = { name: 'a', bar: { name: 'bar' } }
console.log(foo.bar.baz?.name)

トランスパイルするとどうなるかというと下記のようになります。

var _a;

let foo;
foo = { name: 'a', bar: { name: 'bar' } };
console.log((_a = foo.bar.baz) === null || _a === void 0 ? void 0 : _a.name);


完全に勘違いしていた

私の知識不足なんですが、トランスパイルされたコードは普通にこうなるかと思っていました。(一緒に働いているエンジニアさんも勘違いしていたみたい)

foo.bar.baz && foo.bar.baz.name

ここでひとつ懸念が・・・

「これ、使いすぎるとバンドルサイズやばくね??」

「あれ?じゃあ、実行速度はどうなんよ??」

SPAプロジェクトにおいてバンドルサイズは大敵です。もちろんServiceWorkerでキャッシュさせて初回以降のRenderを速くすることは可能ですが、バンドルサイズが大きければ大きいほど初回renderは遅くなってしまいます。

ってことで簡単なテストですが、バンドルサイズと実行速度の検証をしてみました。


検証

実際のReactのプロジェクトにコードを書いてテストをしてみました。


Optional Chaining を使わないパターン

単純に && で評価してくパターンです。正直可読性よくない。

interface Foo {

name: string
bar: {
name: string
baz?: {
name: string
}
}
}

let foo: Foo

const startTime = performance.now() // 開始時間
foo = { name: 'a', bar: { name: 'bar' } }

console.log(foo && foo.bar && foo.bar.baz && foo.bar.baz.name)
console.log(foo && foo.bar && foo.bar.baz && foo.bar.baz.name)
console.log(foo && foo.bar && foo.bar.baz && foo.bar.baz.name)
console.log(foo && foo.bar && foo.bar.baz && foo.bar.baz.name)
console.log(foo && foo.bar && foo.bar.baz && foo.bar.baz.name)
console.log(foo && foo.bar && foo.bar.baz && foo.bar.baz.name)
console.log(foo && foo.bar && foo.bar.baz && foo.bar.baz.name)
console.log(foo && foo.bar && foo.bar.baz && foo.bar.baz.name)
console.log(foo && foo.bar && foo.bar.baz && foo.bar.baz.name)
console.log(foo && foo.bar && foo.bar.baz && foo.bar.baz.name)

const endTime = performance.now() // 終了時間

バンドルサイズ

11.09 KB build/static/js/main.566b4901.chunk.js

実行速度

2.094999999826541


Optional Chainingを使う

const startTime2 = performance.now() // 開始時間

foo = { name: 'a', bar: { name: 'bar' } }

console.log(foo?.bar?.baz?.name)
console.log(foo?.bar?.baz?.name)
console.log(foo?.bar?.baz?.name)
console.log(foo?.bar?.baz?.name)
console.log(foo?.bar?.baz?.name)
console.log(foo?.bar?.baz?.name)
console.log(foo?.bar?.baz?.name)
console.log(foo?.bar?.baz?.name)
console.log(foo?.bar?.baz?.name)
console.log(foo?.bar?.baz?.name)

const endTime2 = performance.now() // 終了時間
console.log('after *****************')
console.log(endTime2 - startTime2)
console.log('*****************')

バンドルサイズ

11.42 KB (+347 B) build/static/js/main.87dedbcc.chunk.js

実行速度

1.4400000000023283

思った通りバンドルサイズは大きくなりました。簡単なテストっていう部分はありますが、実行速度は多少速くなっていますね。

Optional Chainingの使いすぎはバンドルサイズの肥大化に繋がることがわかりました。


Optional Chainingの使い方を工夫する

const startTime3 = performance.now() // 開始時間

foo = { name: 'a', bar: { name: 'bar' } }
const d = foo.bar.baz?.name
console.log(d)
console.log(d)
console.log(d)
console.log(d)
console.log(d)
console.log(d)
console.log(d)
console.log(d)
console.log(d)
console.log(d)
const endTime3 = performance.now() // 終了時間
console.log('after2 *****************')
console.log(endTime3 - startTime3)
console.log('*****************')

バンドルサイズ

11.09 KB (-338 B) build/static/js/main.1e9f4693.chunk.js

実行速度

1.5050000001792796

このやり方であればバンドルサイズを抑えつつ実行速度も速くなっています。


まとめ

babelもprettierも対応していて環境としては十分整っているので、積極的に使っていくべきだとは思います。ただ、先にも説明したようにバンドルサイズは意味なく肥大化してしまう危険性があるので所構わず使っていいわけではなさそうです。用法用量を守って正しく使えば、バンドルサイズも抑えつつ人に優しいコードを書くことができそうです。