😺

Ruby と ECMAScript の配列展開の挙動の違い

2021/04/11に公開

前置き

検証環境

  • Ruby (CRuby) 3.0.1
  • Node.js 14.15.0

配列の展開とは

Ruby ではスプラット演算子 (Splat Operator) 、
ECMAScript ではスプレッド構文 (Spread Syntax) と呼ばれるもので、
配列要素を他の変数や、メソッドの引数の一部として扱うことができるようになるものです。

# Ruby
array = [:a, :b]
array2 = [*array, :c] # Array は `*` を使って展開
pp array2 #=> [:a, :b, :c]

hash = { a: :a, b: :b }
hash2 = { **hash, c: :c } # Hash は `**` を使って展開
pp hash2 #=> {:a=>:a, :b=>:b, :c=>:c}

def some_method(x, y, z)
  puts "x: #{x}, y: #{y}, z: #{z}"
end

some_method(*array2) #=> x: a, y: b, z: c
// JavaScript, TypeScript
const array = ['a', 'b']
const array2 = [...array, 'c']
console.log(array2) //=> [ 'a', 'b', 'c' ]

// Object の展開は ES2018 以降で可能
const object = { a: 'a', b: 'b' }
const object2 = { ...object, c: 'c' }
console.log(object2) //=> { a: 'a', b: 'b', c: 'c' }

const someFunction = (x, y, z) => {
  console.log(`x: ${x}, y: ${y}, z: ${z}`)
}
someFunction(...array2) //=> x: a, y: b, z: c

挙動の違い

非反復可能オブジェクトを渡したときの挙動が異なる

おおよそ似たような場面で使えるこれらの配列展開ですが、
Ruby と ECMAScript では「非反復可能オブジェクト」を渡したときの挙動が異なります

  • Ruby: 要素1の配列として扱われ、展開される

    # Ruby
    a = 1
    
    def some_method(x)
      x * 2
    end
    
    pp some_method(*a) #=> 2
    
  • JavaScript: 実行時に TypeError が発生する

    // JavaScript
    const a = 1
    
    const someFunction = (...args) => args.reduce((acc, arg) => acc + arg)
    someFunction(...a) // Uncaught TypeError: Found non-callable @@iterator
    
  • TypeScript: 構文解析時点で TS2461 エラーが発生する

    // TypeScript
    const a: number | number[] = 1
    
    const someFunction = (...args: number[]) => args.reduce((acc, arg) => acc + arg)
    someFunction(...a) // error TS2461: Type 'number' is not an array type.
    

ECMAScript における実行時エラーの回避策

  1. Array.prototype.flat() を使う
    0 or 1次元の配列であれば、関数を呼び出す側で配列化してから展開することで、回避はできます
    // JavaScript
    let a = 1
    
    const someFunction = (...args) => args.reduce((acc, arg) => acc + arg)
    console.log(someFunction(...[a].flat())) //=> 1
    
    a = [1, 2, 3]
    console.log(someFunction(...[a].flat())) //=> 6
    
  2. number | number[] なんて型の変数を定義しない
    要素が1つであっても、number[] として定義しましょう
  3. その他
    もっといい感じの方法があったら教えて下さい

参考

Discussion