Kotlin/JS TIPS - JavaScript ライブラリへの型情報の付与

概要

JavaScript ライブラリを使用する際に、型情報を付与する方法について説明します。 前回の記事で JavaScript ライブラリの使用方法を説明しましたが、 静的型付けによる恩恵を得ずに JavaScript の柔軟性を重視した使用方法としていました。 型情報を付与することによって、以下の恩恵を得られる様にしていきます。

ライブラリ開発者の手間を増やして、ライブラリ利用者の手間を減らしていきます。

前回の記事:

nosix.hatenablog.com

目次

確認環境

  • IntelliJ IDEA Community 2017.2
  • Kotlin 1.1.3-2
  • Gradle 3.5
    • Groovy 1.4.10

参考情報

解説

json 関数と Json インターフェース

JavaScript では {} によってオブジェクト生成を行う。 Kotlin/JS では json 関数によってオブジェクトを生成できる。 生成されたオブジェクトは Json インターフェース を実装したオブジェクトであり、 [] による操作を行える。 以下は SampleClient.kt の一部である。

fun run() {
    val option = json()
    option["el"] = "#example"
    option["data"] = json("message" to "Hello World")
}

生成される JavaScript は以下の様になっている。

function run() {
    var option = json([]);
    option['el'] = '#example';
    option['data'] = json([to('message', 'Hello World')]);
}

json 関数のソースコード (Kotlin) は以下になっており、js("({})") でオブジェクトを生成していることがわかる。

public fun json(vararg pairs: Pair<String, Any?>): Json {
    val res: dynamic = js("({})")
    for ((name, value) in pairs) {
        res[name] = value
    }
    return res
}

また、Json インターフェースのソースコード (Kotlin) は以下になっている。

public external interface Json {
    operator fun get(propertyName: String): Any?
    operator fun set(propertyName: String, value: Any?): Unit
}

external を使うことで JavaScript のコードをそのまま使用することができる。 get/set を定義することで [] による参照/代入を可能にしている。

型情報の付与

json 関数の仕組みを応用して以下のとおりに記述することで、Json コンストラクタに見せかけることができる。

fun Json(): Json = js("({})")

fun run() {
    val option = Json()
    option["el"] = "#example"
}

さらに、external を使うことで JavaScript のコードをそのまま使用できる性質を利用して、 [] を使わずにオブジェクトに値を設定できる様にする。 例えば、以下の様に。

fun <T : Json> Json(): T = js("({})")

external interface VueOption : Json {
    var el: Any
    var data: Json
}

external interface Model : Json {
    var message: String
}

fun run() {
    val option = Json<VueOption>()
    option.el = "#example"
    option.data = Json<Model>().apply {
        message = "Hello World"
    }
    option["computed"] = Json() // VueOption は Json インターフェースを実装しているので [] を使える
}

生成された JavaScript は以下のとおり。 external を使用した部分は簡潔なコードとなっている。 コード上に VueOptionModel インターフェースは現れない。

  function Json() {
    return {};
  }

  function run() {
    var option = Json();
    option.el = '#example';
    var $receiver = Json();
    $receiver.message = 'Hello World';
    option.data = $receiver;
    option['computed'] = Json();
  }

さらに、初期化処理を行う関数を引数 (init) にとる Json 関数を追加することで、 ()apply を省略できる。

fun <T : Json> Json(init: T.() -> Unit): T = Json<T>().apply(init)

fun run() {
    val option = Json<VueOption> {
        el = "#example"
        data = Json<Model> {
            message = "Hello World"
        }
        this["computed"] = Json()
    }

external interface を使うことで型情報を付与し、 補完機能の恩恵を得えつつ、 誤った型の値を代入する危険性を減らすことができる。

Vue.js に適用する

さらに、Vue 関数を SampleClient.kt に追加すると、Vue の初期化が簡潔になる。

@JsNonModule
@JsModule("vue")
external class Vue(option: VueOption)

fun <T : Json> Json(): T = js("({})")
fun <T : Json> Json(init: T.() -> Unit): T = Json<T>().apply(init)

external interface VueOption : Json {
    var el: Any
    var data: Json
}

external interface Model : Json {
    var message: String
}

fun Vue(init: VueOption.() -> Unit): Vue = Vue(Json<VueOption>().apply(init))

fun run() {
    val vm: dynamic = Vue {
        el = "#example"
        data = Json<Model> {
            message = "Hello World"
        }
    }
    vm.message = "Hello Kotlin World"
}

前回の記事と比べると随分と簡潔になったと思う。 しかし、検討すべき課題は残っている。 補完機能の恩恵は受けられるようになったが、型安全という面では十分とは言えない。 上記の例で言えば、elString もしくは HTMLElement であるため、Any にしている。 また、vm.message を参照するために dynamic にしている。 JavaScript の動的な側面を扱う方法については検討が必要である。

ソースコード

Release 2017-08-01-143728 · nosix/kotlin-js · GitHub