Scala.jsで関数の引数にオブジェクトを渡すときの書き方について考える。
js.Objectを用いて定義せずに詳細に定義することを考える。
結論としてはこんな感じ
- 引数には Non-native JS trait を用いる
- プロパティのありなしを区別したいなら区別する分だけtraitを作る
- applyメソッドを用意しておくと便利
- 構造的部分型は使えない。(使う方法が思いつかない)
- nullは現状扱いづらいので極力使いたくない。別の仕組みが必要
- Optionはundefinedとマッピングされている
 
FlowTypeの型定義を対比させつつ書く(参考 https://qiita.com/eielh/items/a6c162a60305d7668fd2)
前準備
登場するFacadeはすべてconsole.logにわりあててます
module.exports = {
  hoge: console.log,
  hoge2: console.log,
  hoge3: console.log,
};
Facade
@JSImport("hoge.js", JSImport.Namespace)
@js.native
object Hoge extends js.Any {
  /* aの値はundefinedやnullになってもよいが必須 */
  /* function hoge(x: {| a: ?number |}) */
  def hoge(hoge: HogeArgs): Unit = js.native
  /* a はあってもなくてもよい。nullはだめ */ // null はだめは何らかの仕組みを考えないと制限できない。
  /* function hoge2(x: {| a?: number |}) */
  def hoge2(hoge: Hoge2Args): Unit = js.native
  /* a は必須 */
  /* function hoge3(x: {| a: number |}) */
  def hoge3(hoge: Hoge3Args): Unit = js.native
}
function hoge(x: {| a: ?number |})
aというプロパティは必要だけど値はnullやundefinedであっても構わない。
利用例
object Hoge1 {
  import Hoge._
  def sample(): Unit = {
    println("hoge1")
    hoge(HogeArgs(a = 1))  // => { a: 1 }
    hoge(HogeArgs(a = js.undefined))  // => { a: undefiend }
    hoge(HogeArgs(a = null))  // => { a: null } // しかし nullはかきたくない。
    hoge(HogeArgs()) // => {a: undefiend }
  }
}
Scala上のコードでnullは使いたくないが、nullにマッピングされているのはnullのみ。
代わりの値を用意したほうが良さそうだけど、今回は無視。
引数用のtrait用意
trait HogeArgs extends js.Object {
  val a: js.UndefOr[Int] = js.undefined
}
applyの用意
object HogeArgs {
  def apply(a: js.UndefOr[Int] = js.undefined): HogeArgs = {
    val a_ = a
    new HogeArgs {
      override val a: js.UndefOr[Int] = a_
    }
  }
}
a_に束縛しているのは名前がかぶってしまうためです。
他によい回避方法があったら教えてください。
デフォルト引数でaの指定がなければundefinedになるようになっています。
function hoge2(x: {| a?: number |})
aはあってもなくてもよいが、値はnullになってはだめ。undefinedはないのと同じ扱い。
しかし、Scalaの都合上、nullは渡せてしまう。
ので、これを使う意味はあまりない。
利用例
object Hoge2 {
  import Hoge._
  def sample(): Unit = {
    println("hoge2")
    hoge2(Hoge2Args(a = 1)) // => { a: 1 }
    // hoge2(Hoge2Args(a = js.undefined)) // compile error
    // hoge2(Hoge2Args(a = null)) // compile error
    hoge2(Hoge2Args()) // => {}  // { a: undefined } とは等価と考える
  }
}
引数用のtrait用意
trait Hoge2Args extends js.Object
trait Hoge2ArgWithA extends Hoge2Args {
  val a: Int
}
引数を区別するには複数traitを作らざるを得ない。
あってもなくても良い値が増えてくると組み合わせが爆発するので、js.UnedfOr[Int]にしたほうが楽かもしれない。その場合、nullが渡せてしまう。
// 別解
trait Hoge2Args extends js.Object
trait Hoge2ArgWithA extends Hoge2Args {
  val a: Int
}
object Hoge2 {
  import Hoge._
  def sample(): Unit = {
    println("hoge2")
    hoge2(Hoge2Args(a = 1)) // => { a: 1 }
    hoge2(Hoge2Args(a = js.undefined)) // => { a: undefined }
    hoge2(Hoge2Args(a = null)) // => { a: null } // compile errorにしたい
    hoge2(Hoge2Args()) // => {}  // hogeとは結果がちがう
  }
}
applyの用意
object Hoge2Args {
  def apply(a: js.UndefOr[Int]): Hoge2Args = {
    val a_ = a
    new Hoge2ArgWithA {
      override val a: js.UndefOr[Int] = a_
    }
  }
  def apply(): Hoge2Args = {
    new Hoge2Args {}
  }
}
オーバーロードしてしまえばよい。
function hoge3(x: {| a: number |})
aは必須。
利用例
object Hoge3 {
  import Hoge._
  def sample(): Unit = {
    println("hoge3")
    hoge3(Hoge3Args(a = 1)) // => { a: 1 }
    // hoge3(Hoge3Args(a = js.undefined)) // compile error
    // hoge3(Hoge3Args(a = null)) // compile error
    // hoge3(Hoge3Args()) // compile error
  }
}
引数用のtrait用意
trait Hoge3Args extends js.Object {
  val a: Int
}
applyの用意
object Hoge3Args {
  def apply(a: Int): Hoge3Args = {
    val a_ = a
    new Hoge3Args {
      override val a: Int = a_
    }
  }
}
特にコメントはない。
まとめ
頭にかいた通りなので、特にない。
Non-native JS traitを使うのは型安全するためであるが、詳細は関連記事など参照ください。