Compare @JsName, @JsModule, and @JsExport. When would you reach for each, especially to import from an npm package?
answer
- @JsExport = OUT (Kotlin → JS)
- @JsModule = IN (npm → Kotlin), on external
- @JsName = rename, both directions, fixes overload clashes
- @JsNonModule for global/non-module builds
- Direction is the key to picking the right one
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.
solid answer
~50 sThese three solve different directions of interop. **@JsExport** publishes a Kotlin declaration to the JS/TS world with a stable name. **@JsName("...")** overrides the JS-visible name of a declaration; common uses are giving an exported member a specific JS name, disambiguating overloads (which JS can't represent), or matching an external JS symbol's exact name. **@JsModule("package-name")** is the inbound counterpart: applied to an `external` declaration, it tells the compiler that this symbol is provided by an npm module and should be imported from it (`import ... from 'package-name'`), so you can call third-party JS as typed Kotlin. You frequently combine them: `@JsModule("lodash")` with `external` declarations to bind a library, and `@JsName` to fix individual member names. @JsModule pairs with `@JsNonModule` if you also need a non-module (global) build. So: @JsExport = export Kotlin; @JsModule = import npm; @JsName = rename in either direction.
code
kotlin · 14 lines// Inbound: bind an npm package
@JsModule("uuid")
@JsNonModule
external object uuid {
@JsName("v4")
fun v4(): String
}
// Outbound: publish Kotlin, disambiguating overloads with @JsName
@JsExport
object Ids {
@JsName("newUuid")
fun newUuid(): String = uuid.v4()
}go deeper
Knows @JsExport sends Kotlin to JS and that the other two relate to JS interop.
Correctly maps each annotation to its direction and uses @JsModule+external to import an npm package.
Explains overload disambiguation via @JsName and the @JsModule/@JsNonModule pairing for multi-format libraries.
Designs a clean typed Kotlin facade over external npm APIs, isolating dynamic at the edge and keeping the exported surface stable.
## Three annotations, three jobs Interop has a **direction**. Keep that straight and these annotations stop being confusing. ### @JsExport — Kotlin → JS (outbound) Publishes a top-level Kotlin declaration with a stable, un-mangled name and (with TS declarations enabled) a `.d.ts` entry. Covered above. It does **not** import anything. ### @JsName — rename a symbol (both directions) `@JsName("newName")` overrides the JS-visible identifier of a declaration. Uses: - **Disambiguate overloads**: JS has no overloading, so two Kotlin functions with the same name collide when exported. `@JsName` gives each a distinct JS name. - **Match an external symbol**: when binding existing JS whose name isn't a valid Kotlin identifier or differs from what you want to call it in Kotlin. - **Polish the public API**: rename an exported member to a JS-idiomatic name. ```kotlin @JsExport class Box { @JsName("fromInt") constructor(v: Int) @JsName("fromString") constructor(v: String) } ``` ### @JsModule — JS npm → Kotlin (inbound) `@JsModule("package-name")` is placed on an **`external`** declaration to say: *this declaration is implemented by the npm module `package-name`; import it from there.* The compiler emits the appropriate module import. ```kotlin @JsModule("lodash") external fun chunk(array: Array<Int>, size: Int): Array<Array<Int>> ``` For a default export or a whole module object you often bind it to one name: ```kotlin @JsModule("axios") external val axios: dynamic ``` #### @JsNonModule If your build must also work without a module system (the library is a global), add `@JsNonModule` alongside `@JsModule` so the symbol resolves as a global too. You typically use both for libraries shipped in multiple formats. ## How they combine for an npm import ```kotlin @JsModule("date-fns") @JsNonModule external object dateFns { @JsName("format") fun formatDate(date: Date, pattern: String): String } ``` - `@JsModule` says where it comes from, - `external` says 'don't generate a body, it exists in JS', - `@JsName` aligns the Kotlin member with the real JS function name. ## Quick decision table - Publishing Kotlin to JS/TS → **@JsExport**. - Importing/binding an npm package → **@JsModule** (+ `external`, often `@JsNonModule`). - Renaming any symbol (overload clash, name mismatch, API polish) → **@JsName**. Note: `external` (declaring that something exists in JS without a body) is the foundation @JsModule builds on, but the deep `external`/`dynamic` mechanics are a sibling topic.
- Why might you need @JsName on an exported class that has two constructors taking different types?JavaScript has no constructor overloading, so the two would collide; @JsName gives each constructor a distinct, callable JS name.
- What is @JsNonModule for?It lets a @JsModule-bound declaration also resolve as a global symbol when the build is consumed without a module system (e.g., a plain script include).
@JsExport is exporting goods, @JsModule is importing goods from a named foreign supplier (the npm package), and @JsName is relabeling the package so both sides agree on the name.
saying these in an interview costs you the question
- Using @JsModule to export Kotlin (wrong direction)
- Putting @JsModule on a declaration that isn't external
- Thinking @JsName imports an npm package
- Not knowing JS lacks overloading, so @JsName is needed to disambiguate
- Forgetting @JsNonModule for non-module/global consumption