こんにちは。@kyo_agoです。
だいぶScalaJSでの開発知見が溜まってきたので共有したいと思います。
d.tsから.scalaへの変換
TypeScriptを使っていると真っ先に思い浮かぶのが「型定義は?」ということだと思います。
これに関してはScalaJSでもTypeScriptと同じようにScala側のコードからJS側のコードを呼び出す際に型定義が必要になります。
基本的には TypeScript Importer for Scala.js を使うことでd.tsから.scalaのコードを出力できますが、一部の機能は変換できないためエラーになったり定義が出力されなかったりします。
( Type Definition Importer for Scala.js でWeb上から試すことができますが、必ずしも最新版でない可能性があるので注意してください)
サポート範囲に関しては scala-js-ts-importer/samples をみてもらう方がわかりやすいかもしれませんが、特に以下の点に注意してください。
- Advanced Types · TypeScript に書かれている内容に関してはかなりサポートされていません。
- ScalaJSで表現できる範囲であってもscala-js-ts-importerがサポートしていないため出力が省略される記法も多いです。
変換結果に関しては必ず確認することをおすすめします。
npm経由でinstallしたライブラリの使い方
まず、JS側のライブラリの読み込み方によって定義の仕方が若干変わります。
基本的には Write facade types for JavaScript APIs - Scala.js ここに書かれているとおりですが、変換結果に自信が持てない場合、事前にjs側で読み込んでおいてScala側からは呼び出すだけのほうが簡単かもしれません。
// JS側で定義されたclass Hogeのfacade @js.native // window object上に定義された名前は以下で定義するのでここではどんな名前でも良い trait HogeClassBase extends js.Object { def huga(): String = js.native } // Globals object(ブラウザ環境の場合、window object)の型定義 @js.native @JSGlobalScope object Globals extends js.Object { // window.HogeClassの定義 val HogeClass: HogeClassBase = js.native } object TutorialApp { def main(args: Array[String]): Unit = { // window object経由でJS側のHogeClassを呼び出す val hoge = new Globals.HogeClass() println(hoge.huga()) } }
ScalaJS側の型定義の書き方
基本的な書き方は Write facade types for JavaScript APIs - Scala.js や Non-native JS types (aka Scala.js-defined JS types) - Scala.js をみてもらうほうがわかりやすいと思いますが、ここでは実際記述していて困った部分を書いておこうと思います。
Union type
TypeScriptで言うところの|ですが、これはScalaJSでも同じように書けます。
import scala.scalajs.js.| @js.native trait HogeClassBase extends js.Object { // 戻り値はStringかIntのいずれか def huga(): String | Int = js.native }
ただ、これはScalaの型定義としてはjs.|[String, Int]
という形で定義されるため、そのままではStringとしてもIntとしても使うことができません。
これに関しては戻り値を.isInstanceOf[String]
等で分岐した後.asInstanceOf[String]
で変換することで望みの型で使うことができるようになります。
(.asInstanceOf
での変換はJSで実行時の方が違うと実行時エラーになるため必ず先に.isInstanceOf
で判定を行ってください)
import scala.scalajs.js.| object Hoge { def main(huga: String | Int) { if (huga.isInstanceOf[String]) { println(huga.asInstanceOf[String]) } } }
また、.asInstanceOf
や.isInstanceOf
の引数の型が元の定義と違っていてもコンパイルは成功するので注意してください
import scala.scalajs.js.| object Hoge { def main(huga: String | js.Object) { // 実行はされないがコンパイルは通る if (huga.isInstanceOf[Int]) { println(huga.asInstanceOf[String]) } // 警告なくコンパイルできて実行時エラーになる if (huga.isInstanceOf[String]) { println(huga.asInstanceOf[Int]) } } }
もしmatch
を使う場合、一旦Any
に変換することで記述することができます。
import scala.scalajs.js.| object Hoge { def main(hoge: String | Int) { (hoge: Any) match { case huga: String => println(huga) case huga: Int => println(huga) } } }
他にも.toString
を使うこともできます。
import scala.scalajs.js.| object Hoge { def main(hoge: String | Int) { println(huga.toString) } }
scala.scalajs.js.|
の扱いに関してはStack Overflowの質問に対する作者の返信も参照してみてください。
Nullable types
基本的にはOption型で定義すれば問題ありません。
注意点として、nullはNoneと判定されますが、undefinedはSomeとして判定されます。
// nullが帰るのでnoneが出力される Option(js.Dynamic.global.document.getElementById("")) match { case Some(_) => println("some") case None => println("none") } // undefinedはsomeが出力される Option(js.Dynamic.global.undefined) match { case Some(_) => println("some") case None => println("none") }
そのため、以下のようなコードは実行時エラーになる可能性があるため注意してください。
// window.Hogeがある場合実行したい Option(js.Dynamic.global.Hoge) match { // window.Hogeが存在しない場合でも実行される(実行時エラー) case Some(_) =>js.Dynamic.global.Hoge.asInstanceOf[js.Function0[Unit]]() case None => }
この場合、以下のように判定してください。
// hasOwnPropertyで判定する if (js.Dynamic.global.hasOwnProperty("Hoge")) { js.Dynamic.global.Hoge.asInstanceOf[Hoge]() }
String / Numeric Literal Types, Index types
TypeScriptで言うところの以下のような定義です。
// String Literal Types type Easing = "ease-in" | "ease-out" | "ease-in-out"; // Numeric Literal Types function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {} // Index types interface Event { click: string; hover: number; } function addEvent<K keyof Event>(name: K): Event[K] {}
これは現状のところScalaJSではサポートされていません。
(StringやIntとして定義することになります)
これに関してはScala側の制限でもあるため、ScalaJSでのサポートは期待できないかもしれません。
ここで問題になるのがEventEmitter等の型定義です。
例えば、ScalaJSの型定義ではdocument.addEventListenerの第一引数がStringになっており、任意のevent nameを指定できることになっています。
(callbackの引数は型引数で定義できる)
この問題に関して、個人的にはjsのAPIを呼び出すScala層を置いて、Scala側のコードで制限をかけています。
@js.native @JSGlobal class EventEmitter extends js.Object { def on(name: String, callback: js.Function): Unit = js.native } class Event() { private val ee = new EventEmitter() def onClick(callback: js.Function) { ee.on("click", callback) } // 他に呼び出し可能なeventを定義していく }
Error
JS側のエラーは以下のようにScala側のErrorへ変換できます。
hoge.onError((error: js.Error) => { if (error) { throw js.JavaScriptException(e) } })
これに関してはStack Overflowの質問に対する作者の返信も参照してみてください。
debugger statementの書き方
JSではdebugger
を書くことでDevToolsのコード実行を停止させてデバッグすることが多いですが、ScalaJSでは以下のように記述することで同じようにコード実行を停止させることができます。
js.special.debugger()
console.logの書き方
ScalaのprintlnはScalaJSでも使用できますが、出力がStringに変換されるためDevToolsのconsoleで確認するには情報が不足します。
(Objectを展開したりできない)
JSでconsoleにlogを出すにはconsole.logを使用しますが、ScalaJSでは以下のような記述でconsole.logを呼び出すことができます。
js.Dynamic.global.console.log("aaaa")
js.Dynamic.global
の記述がちょっと長いですが、以下のような記述で省略することもできます。
import js.Dynamic.{ global => g } g.console.log("aaaa")
Object literalの書き方
JS側のAPI等でJSのObject literalが必要な場合、Scala側からは以下のように記述することができます。
val obj = js.Dynamic.literal( hoge = "hoge", huga = 111 )
Object literalのpropertyを参照するには先にtraitを記述して変換します。
@js.native trait HogeObj extends js.Object { val hoge: String = js.native val huga: Int = js.native } // 型が一致していなくてもコンパイルは通りますが、実行時にエラーになるため注意してください。 val obj: HogeObj = js.Dynamic.literal( hoge = "hoge", huga = 111 ) println(obj.hoge) // "hoge"
trait内の定義をvarにするとpropertyを上書きできます。
@js.native trait HogeObj extends js.Object { var hoge: String = js.native val huga: Int = js.native } val obj: HogeObj = js.Dynamic.literal( hoge = "hoge", huga = 111 ) obj.hoge = "bar" // hugaはvalなのでコンパイル時にエラーになります。 obj.huga = 222
Object literalの展開方法
JS側のAPIから値を受け取った場合、Scala側からはそのままではアクセス出来ないため、変換する必要があります。
これは基本的に.asInstanceOf
で変換すればいいのですが、「propertyの有無で型を分けたい」という場合には困ることになります。
(.asInstanceOf
でいきなり変換すると実行時エラーになる可能性がある)
その場合以下のように変換することで安全に扱うことができます。
@js.native trait Hoge extends js.Object { // hogeは存在しない可能性があるので、js.UndefOrでくるんだ上でjs.undefinedで定義する val hoge: js.UndefOr[String] = js.undefined } // messageはhogeがなくても実行時エラーにならない def Hoge(message: Hoge) { // js.UndefOrの持つtoOptionを使ってアクセスする message.hoge.toOption.foreach(println) }
fetch
直接fetchを扱うのではなく、ScalaJS側で提供しているAjax
を使用するようです。
import org.scalajs.dom.ext.Ajax def get(url: String): Unit = { Ajax.get(url) map (r => println(r.responseText) ) onFailure { case dom.ext.AjaxException(r) => println("Error:" + r.response) } }
ajax - scala.js with scala.js-react. Is there fetch method? - Stack Overflow
まとめ
色々問題点を上げていますが、現在でも十分実用的なので「Scalaでフロントエンドを書きたい」という場合には検討してみてください。