How does JavaScript interop in Kotlin/Wasm (wasmJs) differ from Kotlin/JS, and what are the rules for external declarations?
answer
- Wasm = real FFI boundary; JS = same language
- JsAny family: JsString/JsNumber/JsBoolean/JsReference
- No dynamic in Kotlin/Wasm
- external declarations map to JS, no body
- js("...") needs a constant string
basics
~20 sBoth let Kotlin call JavaScript, but Kotlin/Wasm runs as a real Wasm module, so values crossing the boundary are limited and use a special JsAny type family. Kotlin/JS compiles to JavaScript directly, so its interop is looser and supports dynamic.
solid answer
~40 sIn **Kotlin/JS** the output *is* JavaScript, so interop is loose: you can use the `dynamic` type and pass most Kotlin objects across freely. In **Kotlin/Wasm (wasmJs)** the output is a Wasm module living beside the JS host, so the boundary is stricter and typed. JS references are represented by the **`JsAny`** type hierarchy (`JsString`, `JsNumber`, `JsBoolean`, `JsArray`, `JsReference<T>`), and there is **no `dynamic`**. You declare JS APIs with `external` (interfaces, classes, functions, properties), and you can inline small JS snippets with the **`js("...")`** function whose body must be a constant string expression. Conversions use helpers like `.toJsString()` / `String.toString()` on `JsString`, and `external` function parameters/returns must be Wasm-interop-compatible types. Functions passed to JS must be wrapped appropriately. This stricter, statically typed boundary is the main interop difference.
code
kotlin · 10 linesexternal interface JsConsole : JsAny {
fun log(msg: JsString)
}
external val console: JsConsole
fun greet(name: String) {
console.log("Hello, $name".toJsString())
}
fun now(): Double = js("Date.now()")go deeper
Knows both can call JS but that Kotlin/Wasm is a separate module with a stricter boundary.
Can declare external APIs, use JsString/JsAny conversions, and use js("..."), and explains the absence of dynamic.
Explains the typed FFI rationale, JsReference for object handles, lambda adapters, and migration pitfalls from Kotlin/JS.
Reasons about interop performance, boundary-crossing cost, and architecting code to minimize Wasm↔JS traffic in a real app.
## Why the boundary differs - **Kotlin/JS** compiles Kotlin **into JavaScript**. There is no real boundary — your Kotlin *is* JS at the end — so it offers the loose **`dynamic`** type (turns off type checking) and passes objects freely. - **Kotlin/Wasm (`wasmJs`)** compiles to a **WebAssembly module** that runs *next to* a JavaScript host. Crossing from Wasm to JS is a genuine FFI boundary, so it must be **typed and restricted**. ## The JsAny type family In `wasmJs`, JavaScript values are modeled by an external type hierarchy rooted at **`JsAny`**: - `JsString`, `JsNumber`, `JsBoolean` — JS primitives. - `JsArray<T>` — JS arrays. - `JsReference<T>` — an opaque handle wrapping a Kotlin object so it can be held by JS and brought back. There is **no `dynamic`** in Kotlin/Wasm. You must convert explicitly, e.g. `"hi".toJsString()` produces a `JsString`, and a `JsString` converts back with `.toString()`. ## external declarations You describe JS APIs with the **`external`** keyword: ```kotlin external interface Console : JsAny { fun log(message: JsString) } external val console: Console external fun encodeURIComponent(value: JsString): JsString ``` Rules to remember: - `external` members have **no body** — they map to JS. - Parameter and return types must be **Wasm-interop-compatible**: `JsAny` subtypes, primitives that map directly (e.g. `Int`, `Double`, `Boolean`), `String` (auto-converted), or function types wrapped for JS. - `external interface`s typically extend `JsAny`. ## The js("...") intrinsic For small inline JS you use the **`js("...")`** function. Its argument must be a **compile-time constant string**: ```kotlin fun alertHi() { js("alert('hi')") } fun nowMillis(): Double = js("Date.now()") ``` The compiler emits the snippet into the JS glue. You cannot build the string dynamically. ## Passing Kotlin lambdas to JS Function types crossing to JS must be interop-compatible; the compiler generates the adapter so a Kotlin lambda can be invoked from JS. You typically expose them through `external` function parameters typed as function types of JsAny-compatible types. ## Summary of differences vs Kotlin/JS - **No `dynamic`** in Wasm; everything is statically typed via `JsAny`. - **Explicit conversions** (`toJsString()`, etc.) instead of implicit object sharing. - **`js("...")`** requires a constant string; Kotlin/JS allowed richer `js()`/`dynamic` usage. - The Wasm boundary is a real FFI, so only a constrained set of types may cross.
- Why was dynamic removed in Kotlin/Wasm?Because Wasm is a typed binary module separate from JS; there is no untyped JS object graph to lean on, so the boundary must be statically typed via the JsAny hierarchy.
- What is JsReference<T> used for?It wraps a Kotlin object as an opaque handle so JS can hold onto it and pass it back into Wasm later, where it can be unwrapped to the original Kotlin instance.
Kotlin/JS is two people speaking the same language; Kotlin/Wasm is two people who need a customs checkpoint (JsAny) and a phrasebook (explicit conversions) to exchange anything.
saying these in an interview costs you the question
- Claiming you can use dynamic in Kotlin/Wasm
- Passing arbitrary Kotlin objects across the boundary without JsReference/conversion
- Building the js("...") string at runtime from variables
- Assuming Kotlin/JS and Kotlin/Wasm interop are identical
- Forgetting external interfaces should extend JsAny