動くけど危ない。TypeScript×JavaScriptの注意ポイント
業務中、以下のようなコードを見かけました。
(実際はNext.jsのコードでしたが、JavaScriptの言語仕様に関わる部分だけをシンプルに抜き出しています。)
const tags: string[] = ['hoge']
console.log(`https://example.com/${tags}`)
/* https://example.com/hogeと出力されるが、tagsの要素数が2つ以上になるとカンマで要素が連結され、https://example.com/hoge,fugaのようになる。
偶然、要素数が2つ以上のものがなくて設定されたリンクが404エラーにならなかった。
しかし、配列の要素数が増えたときにリンク切れを引き起こすリスクを含んでいた。
*/
今回の記事では、上記のコードのように、JavaScript由来の言語仕様を知らないと危ないコードのパターンを思いつく限りまとめてみたいと思います。
数値の小数点演算の誤差
const result = 0.1 + 0.2
console.log(result) // → 0.30000000000000004
型はnumber型で正しいですが、計算結果が直感に反します。
金額計算や数量の管理などでは、特に注意が必要です。
原因についてはこちらの記事にも言及されている通り、TypeScript(JavaScript)のnumberがIEEE754浮動小数点数ベースだから起きる誤差です。
※基本情報技術者試験の勉強で出てくるやつです。
これはTypeScript(JavaScript)に限らず、広く起こる問題です。
オブジェクトのキーが文字列に変換される
const map: { [key: number]: string } = {}
map[1] = 'one'
map[2] = 'two'
console.log(Object.keys(map)) // → ['1', '2']
JavaScriptのオブジェクトキーは必ず文字列になるので、
TypeScriptでキーにnumber型を宣言しても、コンパイルされた時点でstring型に変換されます。
解決策としては、Mapを使い、キーと値のペアを定義することです。
これなら、暗黙的にキーがstring型に変換されずに済みます。
Mapでは、元の型情報(たとえばnumber型)が維持されます。
const map = new Map<number, string>()
map.set(1, 'one')
map.set(2, 'two')
console.log([...map.keys()]) // → [1, 2]
配列(オブジェクト)を直接比較すると常にfalse
const a = [1, 2, 3]
const b = [1, 2, 3]
console.log(a === b) // → false
配列に限らず、オブジェクトであればこの事象は起きます。
文字列や数値を直接比較する場合と異なり、データそのものを比較するのではなく、
参照元(インスタンスやメモリ上の配置などとも表現されます)を比較しており、
これが異なるため、falseとなります。
解決策としては、オブジェクトの要素そのものの比較を行うことになります。
その方法としては、lodashのisEqualを使うのが最も楽だと思います。
import _ from 'lodash'
const a = [1, 2, 3]
const b = [1, 2, 3]
console.log(_.isEqual(a, b)) // → true
論理和演算子(||)とNull合体演算子(??)の挙動の違い
下の2つのコードは似ていますが、異なる挙動をします。
const user: { name?: string | null } = {name: ''}
const name = user.name || 'Guest'
console.log(name) // → 'Guest'
const user: { name?: string | null } = {name: ''}
const name = user.name ?? 'Guest'
console.log(name) // → ''
論理和演算子は、左辺がfalsyな値の場合は右辺を返し、
左辺がtruthyな値の場合は左片を返します。
空文字はfalsyな値なので、右辺のGuest
が返されました。
それに対し、Null合体演算子は、左辺がnullもしくはundefinedの際に右辺を返し、
それ以外の場合は左辺を返します。
なので、左辺の空文字が返りました。
まとめると、以下のようになります。
空文字をどのように扱いたいかで、書くべきコードが変わります。
一般に「空文字は有効な入力とみなす」場合はNull合体演算子(??)を使うのがおすすめです。
値 | truthyかfalsyか | 論理和演算子 ( || ) で値が返るか | Null合体演算子 ( ?? ) で値が返るか |
---|---|---|---|
null | falsy | 返らない | 返らない |
undefined | falsy | 返らない | 返らない |
空文字 | falsy | 返らない | 返る |
配列の展開でfor...inがイメージと異なる動きをする
for...inはオブジェクトのキーを出力するため、配列の場合はインデックス番号を出力することになります。
したがって、以下のような書き方をして配列の要素にアクセスしようとしても、
インデックス番号が出てくるだけです。
const arr = ['a', 'b', 'c']
for (const i in arr) {
console.log(i) // → '0', '1', '2'
}
配列の要素にアクセスしたければ、例えば以下のように書けます。
for...ofやforEachを使う書き方の方がシンプルです。
(配列には基本的にfor...ofやforEachを使うべきで、for...inはオブジェクト用です。)
const arr = ['a', 'b', 'c']
for (const i in arr) {
console.log(arr[i]) // → 'a', 'b', 'c'
}
for (const i of arr) {
console.log(i) // → 'a', 'b', 'c'
}
arr.forEach((element) => console.log(element)) // → 'a', 'b', 'c'
// 要素を上書きしたい場合など、インデックスが欲しい場合は .entries() や .keys() を使うと良いです
for (const i of arr.keys()) {
console.log(i, arr[i]) // → 0 'a', 1 'b', 2 'c'
}
for (const [i, value] of arr.entries()) {
console.log(i, value) // → 0 'a', 1 'b', 2 'c'
}
Discussion
むしろ for ... of で
.keys()
するべきみたいなのもあります。(直接 for...of すると配列の要素へのアクセスであり、配列のインデクサへのアクセスとはならない為(要素の上書き目的の場合コメントありがとうございます!
サンプルコードの編集を行い、コメントいただいた旨を追記させていただきました💨