skip to content

JavaScript Interop

Bridging Kotlin compiled to JS or Wasm with the JavaScript world in both directions — calling JS APIs from Kotlin and exporting Kotlin declarations to JS consumers. It is the corner of KMP that web-facing teams care about.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

explore

questions

10

What does the @JsExport annotation do in Kotlin/JS, and why do you need it before JavaScript or TypeScript code can call your Kotlin declaration?

level: juniorimportance: must knowfreq 60%

answer

  1. Kotlin mangles JS names by default
  2. @JsExport = stable name + public surface
  3. Top-level only (or @file:JsExport)
  4. Feeds the generated .d.ts
  5. Only exportable types allowed

basics

~20 s

By default Kotlin renames things when it compiles to JavaScript, so JS code can't find them. Adding @JsExport keeps the real names and generates TypeScript type info, so JS/TS can call your class or function.

solid answer

~40 s

Kotlin/JS uses an internal name-mangling scheme so Kotlin symbols are not reliably reachable from hand-written JavaScript. @JsExport marks a top-level declaration (function, class, object, property) so the compiler emits it with a stable, predictable name and includes it in the module's public surface. When the `dts` / TypeScript-declaration generation is enabled, exported declarations also appear in the generated `.d.ts` file, giving TypeScript consumers real types. Without @JsExport you'd have to rely on `dynamic` access or guess mangled names. @JsExport only works on top-level declarations (or members of an exported class) and restricts you to an 'exportable' subset of Kotlin types — if you export something with a non-exportable signature, the compiler reports an error or warning.

code

kotlin · 11 lines
kotlin
@JsExport
class Greeter(val prefix: String) {
    fun greet(name: String): String = "$prefix, $name"
}

// Generated .d.ts roughly:
// export class Greeter {
//   constructor(prefix: string)
//   readonly prefix: string
//   greet(name: string): string
// }

go deeper

for a junior

Knows @JsExport makes a declaration callable from JS and that names are otherwise mangled.

for a middle

Explains the public-surface + stable-name mechanics, top-level scope, and that it feeds the .d.ts.

for a senior

Adds the exportable-type restriction, @file:JsExport, and the relationship to the TS-declaration Gradle option.

for a principal

Frames @JsExport as the contract boundary for publishing a typed npm/JS API and reasons about API stability across the mangling boundary.

## The problem @JsExport solves When Kotlin compiles to JavaScript, the compiler **mangles** (renames) symbols. Mangling means the Kotlin name `calculateTotal` may become something like `calculateTotal_abc123$` in the emitted JS to avoid clashes and support overloads. Hand-written JavaScript or TypeScript has no way to know that mangled name, so the declaration is effectively invisible from outside Kotlin. ## What @JsExport does `@JsExport` is an annotation you put on a **top-level** declaration — a function, class, `object`, or property — to tell the compiler: - emit it with a **stable, un-mangled name** that matches the Kotlin name (or the `@JsName` override), - include it in the module's **public JS surface**, and - (when TypeScript declaration generation is on) emit it into the generated **`.d.ts`** file so TS consumers get real types. ```kotlin @JsExport fun greet(name: String): String = "Hello, $name" @JsExport class Counter(var value: Int) { fun increment() { value++ } } ``` From JavaScript (assuming module name `app`): ```js import { greet, Counter } from 'app' console.log(greet('Ada')) const c = new Counter(0) c.increment() ``` ## Generating the .d.ts The TypeScript declaration file is produced when the Gradle option for TS declarations is enabled (`generateTypeScriptDefinitions()` on the JS/browser/nodejs target). Only `@JsExport`ed declarations land in it. ## Scope and limits - Applies to **top-level** declarations; you cannot put it on an arbitrary nested member directly, but members of an exported class are exported with it. - You can annotate a whole **file** with `@file:JsExport` to export everything top-level in that file. - Only **exportable types** are allowed in signatures (primitives, `String`, `Boolean`, exported classes, `Array`, function types, etc.). `Long`, non-exported Kotlin classes, and most collection interfaces are not exportable. ## Why not just use `dynamic`? You *can* reach mangled symbols via `dynamic`, but you lose all type safety and the stable contract. `@JsExport` is the supported, type-checked way to publish an API to the JS/TS world.

  • Can you put @JsExport on a private top-level function?
    No useful effect — only public declarations form the exportable surface; the export contract is about the public API, and private members are not exported.
  • How do you export every top-level declaration in a file at once?
    Use the file-level annotation `@file:JsExport` at the very top of the file, before the package declaration.

Without @JsExport, Kotlin symbols are like apartments with scrambled door numbers — the building (module) knows them but visitors can't find them. @JsExport posts the real, stable number on the door.

saying these in an interview costs you the question

  • Claiming @JsExport works on any nested member directly
  • Thinking JS can call un-exported Kotlin by its Kotlin name without dynamic
  • Confusing @JsExport with @JsName (renaming) — they do different jobs
  • Believing @JsExport alone produces a .d.ts without enabling TS declaration generation
  • Saying you can export Long or arbitrary Kotlin collections

context

open as a page

In Kotlin/JS, what is the `external` keyword for, and why do `external` declarations have no body?

level: juniorimportance: must knowfreq 55%

basics

~10 s

external tells the Kotlin compiler that something already exists in JavaScript and is implemented there. You only write its type signature, no body, so Kotlin can type-check your calls without re-implementing it.

open as a page

Which Kotlin types are 'exportable' across an @JsExport boundary, and what happens if you use a non-exportable type like Long or a Kotlin collection in an exported signature?

level: middleimportance: must knowfreq 50%

basics

~20 s

Only simple, JS-friendly types are allowed: numbers, String, Boolean, arrays, function types, and your own exported classes. Things like Long or Kotlin's List aren't exportable, so the compiler warns or errors and the .d.ts gets a fallback type.

open as a page

What is the `dynamic` type in Kotlin/JS, and how does it change the compiler's behavior compared with a normal typed value?

level: middleimportance: must knowfreq 50%

basics

~20 s

dynamic is a special Kotlin/JS type that turns off compile-time checks. You can read any property or call any method on it and the compiler won't complain — correctness is your responsibility, checked only at runtime by JavaScript.

open as a page

Compare @JsName, @JsModule, and @JsExport. When would you reach for each, especially to import from an npm package?

level: middleimportance: should knowfreq 45%

basics

~10 s

@JsExport sends Kotlin OUT to JS. @JsName renames a symbol so it matches a specific JS name (both directions). @JsModule says 'this Kotlin declaration actually lives in an npm package', importing JS INTO Kotlin.

open as a page

You need to call a third-party npm library from Kotlin/JS. When would you write `external` declarations versus using `dynamic`, and what are the tradeoffs?

level: middleimportance: should knowfreq 40%

basics

~20 s

Use external when you can describe the library's shape — you get type checking, autocomplete, and null-safety. Use dynamic for quick or genuinely free-form access where typing isn't worth it, accepting that mistakes only surface at runtime.

open as a page

An exported Kotlin class has two functions named `add` — one taking Int, one taking String. What goes wrong when exported to JS, and how do you produce a clean .d.ts?

level: seniorimportance: should knowfreq 35%

basics

~20 s

JavaScript has no method overloading, so two functions with the same name clash. The compiler complains. You rename each with @JsName, or merge them into one function, so the generated TypeScript file is clean and unambiguous.

open as a page

In a Kotlin Multiplatform project, why can't `dynamic` (or most `external` declarations) live in common code, and what failure modes does misusing `external`/`dynamic` introduce?

level: seniorimportance: should knowfreq 30%

basics

~20 s

dynamic only exists on the JS target, so it can't appear in shared common code that also compiles to JVM or Native. And because external/dynamic bypass real implementation checks, mistakes turn into runtime JavaScript errors instead of compile errors.

open as a page

Explain `definedExternally` and the `js("...")` function. How do they relate to `external` and `dynamic`, and what are the pitfalls?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

definedExternally is a placeholder used in external declarations to say 'the default/value comes from JS, not Kotlin'. js("...") embeds a literal JavaScript expression and returns it as dynamic. Both are interop tools whose mistakes only show up at runtime.

open as a page

You are designing the public API of a Kotlin/JS library consumed by a large TypeScript app. How do you structure the @JsExport surface for a stable, idiomatic, well-typed .d.ts?

level: principalimportance: nice to knowfreq 20%

basics

~20 s

Keep a small, deliberate set of exported types using only JS-friendly types, convert rich Kotlin types at the edge, use @JsName for clean names, and treat the generated TypeScript file as a contract you don't break between versions.

open as a page