Skip to content

Kotlin から JavaScript コードを使用する

Kotlin は当初、Java プラットフォームとの容易な相互運用を目的として設計されました。Kotlin から Java クラスは Kotlin クラスとして見え、Java から Kotlin クラスは Java クラスとして見えます。

しかし、JavaScript は動的型付け言語であり、コンパイル時に型チェックを行いません。Kotlin からは dynamic 型を介して、JavaScript と自由に対話できます。Kotlin の型システムの能力を最大限に活用したい場合は、JavaScript ライブラリに対して外部宣言(external declaration)を作成することで、Kotlin コンパイラや周辺ツールに認識させることができます。

インライン JavaScript

js() 関数を使用すると、Kotlin コード内に JavaScript コードをインラインで記述できます。 例えば:

kotlin
fun jsTypeOf(o: Any): String {
    return js("typeof o")
}

js のパラメータはコンパイル時に解析され、そのまま JavaScript コードに変換されるため、文字列定数である必要があります。そのため、以下のコードは正しくありません:

kotlin
fun jsTypeOf(o: Any): String {
    return js(getTypeof() + " o") // ここでエラーが報告される
}

fun getTypeof() = "typeof"

JavaScript コードは Kotlin コンパイラによって解析されるため、すべての ECMAScript 機能がサポートされているわけではありません。 その場合、コンパイルエラーが発生することがあります。

js() の呼び出し結果は dynamic 型となり、コンパイル時の型安全性は提供されない点に注意してください。

external 修飾子

ある宣言が純粋な JavaScript で記述されていることを Kotlin に伝えるには、external 修飾子を付与します。コンパイラがこのような宣言を見つけると、対応するクラス、関数、またはプロパティの実装が外部(開発者によって、または npm 依存関係を介して)から提供されるものと見なし、その宣言から JavaScript コードを生成しようとしません。そのため、external 宣言は本文(body)を持つことができません。例えば:

kotlin
external fun alert(message: Any?): Unit

external class Node {
    val firstChild: Node

    fun append(child: Node): Node

    fun removeChild(child: Node): Node

    // など
}

external val window: Window

external 修飾子はネストされた宣言に継承されることに注意してください。このため、例に挙げた Node クラスでは、メンバー関数やプロパティの前に external 修飾子がありません。

external 修飾子はパッケージレベルの宣言にのみ許可されます。非 external クラスのメンバーを external として宣言することはできません。

クラスの(静的)メンバーを宣言する

JavaScript では、メンバーをプロトタイプまたはクラス自体のどちらかに定義できます:

function MyClass() { ... }
MyClass.sharedMember = function() { /* 実装 */ };
MyClass.prototype.ownMember = function() { /* 実装 */ };

Kotlin にはこのような構文はありませんが、Kotlin には companion オブジェクトがあります。Kotlin は external クラスのコンパニオンオブジェクトを特別な方法で処理します。オブジェクトを期待する代わりに、コンパニオンオブジェクトのメンバーをクラス自体のメンバーであると想定します。上記の例の MyClass は、次のように記述できます:

kotlin
external class MyClass {
    companion object {
        fun sharedMember()
    }

    fun ownMember()
}

デフォルト値を持つパラメータを宣言する

デフォルト値を持つパラメータがある JavaScript 関数の外部宣言を記述する場合は、definedExternally を使用します。これにより、デフォルト値の生成を JavaScript 関数自体に委任します:

kotlin
external fun myFunWithOptionalArgs(
    x: Int,
    y: String = definedExternally,
    z: String = definedExternally
)

この外部宣言により、1 つの必須引数と 2 つのオプション引数で myFunWithOptionalArgs を呼び出すことができ、デフォルト値は myFunWithOptionalArgs の JavaScript 実装によって計算されます。

JavaScript クラスを継承する

JavaScript クラスは、Kotlin クラスであるかのように簡単に継承できます。external open クラスを定義し、それを非 external クラスで継承するだけです。例えば:

kotlin
open external class Foo {
    open fun run()
    fun stop()
}

class Bar : Foo() {
    override fun run() {
        window.alert("Running!")
    }

    fun restart() {
        window.alert("Restarting")
    }
}

いくつか制限事項があります:

  • 外部基底クラスの関数がシグネチャによってオーバーロードされている場合、派生クラスでそれをオーバーライドすることはできません。
  • デフォルト値を持つパラメータを含む関数をオーバーライドすることはできません。
  • 非外部クラスを外部クラスで継承することはできません。

external インターフェース

JavaScript にはインターフェースの概念がありません。ある関数がパラメータに対して foobar という 2 つのメソッドをサポートすることを期待する場合、実際にはそれらのメソッドを持つオブジェクトを渡すだけです。

静的型付けされた Kotlin では、インターフェースを使用してこの概念を表現できます:

kotlin
external interface HasFooAndBar {
    fun foo()

    fun bar()
}

external fun myFunction(p: HasFooAndBar)

外部インターフェースの典型的なユースケースは、設定オブジェクトを記述することです。例えば:

kotlin
external interface JQueryAjaxSettings {
    var async: Boolean

    var cache: Boolean

    var complete: (JQueryXHR, String) -> Unit

    // など
}

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

external class JQuery {
    companion object {
        fun get(settings: JQueryAjaxSettings): JQueryXHR
    }
}

fun sendQuery() {
    JQuery.get(JQueryAjaxSettings().apply {
        complete = { (xhr, data) ->
            window.alert("Request complete")
        }
    })
}

外部インターフェースにはいくつかの制限があります:

  • is チェックの右側で使用することはできません。

  • reified 型引数として渡すことはできません。

  • クラスリテラル式(I::class など)で使用することはできません。

  • 外部インターフェースへの as キャストは常に成功します。 外部インターフェースへのキャストは、「Unchecked cast to external interface」というコンパイル時の警告を生成します。この警告は @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") アノテーションで抑制できます。

    IntelliJ IDEA は @Suppress アノテーションを自動生成することもできます。電球アイコンまたは Alt-Enter でインテンションメニューを開き、「Unchecked cast to external interface」インスペクションの横にある小さな矢印をクリックします。ここで抑制範囲を選択すると、IDE がそれに応じてファイルにアノテーションを追加します。

キャスト

キャストが不可能な場合に ClassCastException を投げる "unsafe" キャスト演算子 as に加えて、Kotlin/JS は unsafeCast<T>() も提供しています。unsafeCast を使用すると、実行中に型チェックが一切行われません。例えば、次の 2 つのメソッドを考えてみましょう:

kotlin
fun usingUnsafeCast(s: Any) = s.unsafeCast<String>()
fun usingAsOperator(s: Any) = s as String

これらは次のようにコンパイルされます:

javascript
function usingUnsafeCast(s) {
    return s;
}

function usingAsOperator(s) {
    var tmp$;
    return typeof (tmp$ = s) === 'string' ? tmp$ : throwCCE();
}

等価性

Kotlin/JS は、他のプラットフォームと比較して等価性チェックに関する特定のセマンティクスを持っています。

Kotlin/JS では、Kotlin の参照の等価性演算子(===)は、常に JavaScript の厳格な等価性演算子(===)に変換されます。

JavaScript の === 演算子は、2 つの値が等しいことだけでなく、それら 2 つの値の型も等しいことをチェックします:

kotlin
fun main() {
   val name = "kotlin"
   val value1 = name.substring(0, 1)
   val value2 = name.substring(0, 1)

   println(value1 === value2)
   // Kotlin/JS では 'true' を出力
   // 他のプラットフォームでは 'false' を出力
}

また、Kotlin/JS では、ByteShortIntFloat、および Double の数値型はすべて、実行時に Number JavaScript 型で表されます。したがって、これら 5 つの型の値は区別できません:

kotlin
fun main() {
   println(1.0 as Any === 1 as Any)
   // Kotlin/JS では 'true' を出力
   // 他のプラットフォームでは 'false' を出力
}

Kotlin における等価性の詳細については、等価性のドキュメントを参照してください。