【JS】オブジェクト省略記法でnullableでもエラーにしないためにデフォルト引数を使おう
こんにちは!今回のタイトル意味不明ですね。Mizutani(@sirycity)です。オフチョベットとかファルシのルシみたいですね。
今日はそんなオブジェクト省略記法でnullishでもエラーにしないためにデフォルト引数を使う話です。
はじめに
今回はすげーマイナーなJavaScriptの話なので、まずはタイトルにある怪文書について1個ずつ説明していきます。
オブジェクト省略記法
JavaScriptのキーと値が同じ名前なら省略できる記法のことです。例えばこの4つは同じ結果になります。
const name = { first: 'たかし' }
console.log(name.first) // たかし
const { first: firstName } = { first: 'たかし' }
console.log(firstName) // たかし
const { first: first } = { first: 'たかし' }
console.log(first) // たかし
const { first } = { first: 'たかし' }
console.log(first) // たかし
最後のはキーも値もfirstなので省略できてますね。この記法は名前が一緒だから省略できるというよりはむしろ省略するために名前を意図的に合わせにいくみたいな使い方をよくされます。
nullable
nullableはそのまんま、nullになるかもよ?みたいな感じです。jsの場合はundefinedもですね。今回はこんなケースを想定します。
const people = [
{ name: 'たかし' },
{ name: 'ひであき' },
{ name: 'としあき' },
{},
]
people.map(person => console.log(person.name))
// たかし
// ひであき
// としあき
// undefined
4つ目は空なのでundefinedが出力されます。ただまあ、別にエラーとかは起きてないですね。ここまでは良いんです。
nullableで起こるエラー
ネストしているオブジェクトを参照する場合、nullableだとエラーが起きます。例えばこんなかんじ。
const people = [
{ name: { first: 'たかし' } },
{ name: { first: 'ひであき' } },
{ name: { first: 'としあき' } },
{},
]
people.map(person => console.log(person.name.first))
// たかし
// ひであき
// としあき
// TypeError: Cannot read properties of undefined (reading 'first')
person.nameの時点でundefinedなので、undefinedの.firstにアクセスしてそんなのねえよってなるんですね。
optioncal chainingについて
この問題を解決するためにjsにはoptioncal chainingって機能ができました。こんな感じ。
people.map(person => console.log(person.name?.first))
// たかし
// ひであき
// としあき
// undefined
まあすごくざっくり言えば.が?.になっただけです。んで、機能としてはCannot read propertiesのエラーをundefinedに変換するみたいな認識でよいと思います。
省略記法とoptional chainingが共存できない
この話は前の記事でも書いたので簡単に。
簡単に言うと省略記法でoptional chainingは使えないです。言い換えればネストしているnullableなオブジェクトにおいては省略記法が使えないってことです。例えばこんな感じ。
const people = [
{ name: { first: 'たかし' } },
{ name: { first: 'ひであき' } },
{ name: { first: 'としあき' } },
{},
]
people.map(({ name: { first } }) => console.log(first))
// たかし
// ひであき
// としあき
// TypeError: Cannot read properties of undefined (reading 'first')
こんな感じでエラーが起きますが、省略記法はoptional chainingできないんです。そもそもそういう文法が無いんです。
デフォルト引数について
さあここから全く話が変わりますが、次の2つを見てみましょう。
const f = name => console.log(name + 'さん')
f('たかし') // たかしさん
f() // undefinedさん
ごく普通の関数です。2つ目は引数を忘れていますね。まあエラーにはなってないけどね。
jsはこの対策として引数にデフォルト値を設定することができます。こんな感じに。
const f = (name = '匿名') => console.log(name + 'さん')
f('たかし') // たかしさん
f() // 匿名さん
デフォルト引数は変数にしておくこともできます。
const defaultName = '匿名'
const f = (name = defaultName) => console.log(name + 'さん')
f('たかし') // たかしさん
f() // 匿名さん
あるいはnullish coalescingを使う方法もあります。こう。
const f = name => console.log((name ?? '匿名') + 'さん')
f('たかし') // たかしさん
f() // 匿名さん
これだとnullを引数に取れなくなるデメリットがありますが、まあ大抵の場合は大丈夫でしょう。
じゃあ全部nullish coalescingでいいじゃんって?
省略記法とデフォルト引数
デフォルト引数にはメリットが1つあって省略記法と両立できるんです。こんな感じ。
const people = [
{ name: { first: 'たかし' } },
{ name: { first: 'ひであき' } },
{ name: { first: 'としあき' } },
{},
]
people.map(({ name: { first } = { first: '匿名' } }) => console.log(first))
// たかし
// ひであき
// としあき
// 匿名
省略記法を使いながらデフォルトの匿名も設定できました。
ちなみに省略記法なし+optional chaining+nullish coalescingだとこうです。
const people = [
{ name: { first: 'たかし' } },
{ name: { first: 'ひであき' } },
{ name: { first: 'としあき' } },
{},
]
people.map(person => console.log(person.name?.first ?? '匿名'))
// たかし
// ひであき
// としあき
// 匿名
どっちが良いねん
もうほぼ好みですが、ちょっと書いたとおり後者は明示的なnullをデフォルト値で上書きしてしまう可能性があるので前者の方が微妙に良いかなーと思ってます。ただこの挙動が便利な時もあるし、そもそもjsにおいては空値をnullで明示することが妥当か否か?という宗教戦争があります。
身も蓋も無い話
そもそもモジュール結合度の観点から論じればオブジェクトを引数に取るのがあまり良くないです。まあそれは流石に理想論ですね。
さいごに
こういう記事書くたびに思うんですが、いろんな書き方ができることを長所と捉えるか短所と捉えるかってほんとに難しいですよね。ね。以上。