skip to content

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%

answer

  1. Number/Boolean/String/Array/function-type/exported-class = OK
  2. Long NOT exportable (no 64-bit JS int)
  3. List/Map/Set NOT exportable → use Array
  4. Violation → compiler diagnostic + .d.ts fallback type
  5. Convert at boundary: toTypedArray(), Double instead of Long

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.

solid answer

~50 s

The exportable set is intentionally narrow so the API maps cleanly to JS/TS: primitive numerics that fit JS `number` (Int, Short, Byte, Float, Double), `Boolean`, `String`, `Char` maps poorly and is restricted, `Unit`/`Nothing` in return position, `Array<T>` of exportable T, typed arrays, function types `(A)->B` with exportable params, nullable versions of those, and other `@JsExport`ed classes/interfaces/objects/enums. **`Long` is NOT exportable** because JS has no 64-bit integer that maps to it cleanly. Kotlin collection types like `List`, `Map`, `Set` are **not exportable** either — you must expose `Array` instead. Using a non-exportable type in an exported signature produces a compiler diagnostic (warning, or error in stricter setups), and the generated `.d.ts` falls back to a loose type such as `any` / `Nothing`, breaking the typed contract. The fix is to convert at the boundary (e.g., `.toTypedArray()`, expose `Double` instead of `Long`).

code

kotlin · 12 lines
kotlin
// Internal rich model
private data class Account(val id: Long, val balance: Long)

// Exported, JS-shaped DTO
@JsExport
class AccountView(val id: String, val balance: Double)

@JsExport
fun loadAccounts(): Array<AccountView> =
    listOf(Account(1L, 100L))
        .map { AccountView(it.id.toString(), it.balance.toDouble()) }
        .toTypedArray()

go deeper

for a junior

Knows only simple types cross and that List/Long are problematic.

for a middle

Enumerates the exportable set, names Long and collections as excluded, and converts with toTypedArray().

for a senior

Explains the .d.ts fallback degradation and designs a thin exportable adapter layer over rich internal types.

for a principal

Treats the boundary as an API-design discipline (JS-shaped DTOs, exact-id-as-String strategy) and reasons about round-trip fidelity and TS contract honesty.

## What 'exportable' means An **exportable type** is one the Kotlin/JS compiler knows how to represent in the public JS API and in the generated TypeScript declarations (`.d.ts`). Because JavaScript's type system is small, only a curated subset of Kotlin types qualifies. ## The exportable set (current Kotlin 2.x) - **Primitive numbers that map to JS `number`:** `Byte`, `Short`, `Int`, `Float`, `Double`. - **`Boolean`**, **`String`**. - **`Unit`** and **`Nothing`** (in return/position roles). - **`Array<T>`** and typed arrays (e.g., `IntArray`) where the element type is exportable. - **Function types** like `(Int) -> String`, provided parameter and return types are exportable. - **Nullable forms** of the above (`String?` → `string | null`-ish). - **Other `@JsExport`ed declarations**: classes, interfaces, objects, enums. ## What is NOT exportable - **`Long`** — JavaScript has no native 64-bit integer that round-trips to it; Kotlin represents `Long` specially, so it cannot cross the boundary. Expose `Double`, `Int`, or `String` instead. - **Kotlin collection types** — `List`, `MutableList`, `Map`, `Set`, `Collection`, etc. are not exportable. Convert to `Array` (e.g., `list.toTypedArray()`). - **Non-`@JsExport`ed Kotlin classes** appearing in a signature. - **`Char`** is restricted/not generally exportable (it maps awkwardly to JS). - Generic type parameters in unsupported positions, `dynamic` in some contexts, and inline/value classes with non-trivial mapping. ## What happens when you violate it ```kotlin @JsExport fun ids(): List<Long> = listOf(1L, 2L) // BAD: List + Long both non-exportable ``` The compiler emits a diagnostic that the type is **non-exportable**. In the generated `.d.ts`, the offending type degrades to a fallback (commonly `any` / `Nothing` / an opaque type), so TypeScript consumers lose real typing and may get runtime surprises (e.g., `Long` arrives as an opaque object, not a JS number). ## The fix: convert at the boundary ```kotlin @JsExport fun ids(): Array<Double> = arrayOf(1.0, 2.0) // exportable @JsExport fun names(items: List<String>): Array<String> = items.toTypedArray() ``` Keep a thin **adapter layer**: rich Kotlin types internally, exportable types at the `@JsExport` surface. ## Why so strict? The restriction guarantees the generated TS declarations are honest and the values round-trip without surprising boxing. It pushes you to design a deliberate, JS-shaped public API rather than leaking Kotlin internals.

  • How do you safely pass a 64-bit identifier to JS?
    Don't use Long across the boundary. Pass it as a `String` (to preserve exact value) or `Double` if precision loss is acceptable, then convert back inside Kotlin.
  • Can you return a List<String> from an exported function?
    No — List is not exportable. Convert with `.toTypedArray()` to return `Array<String>`.

The export boundary is customs: only goods on the approved list (numbers, strings, arrays, exported classes) pass. Long and List get held at the border until you repackage them.

saying these in an interview costs you the question

  • Claiming Long is exportable
  • Returning Kotlin List/Map/Set from an exported function
  • Assuming a non-exportable type silently maps to a correct TS type
  • Not knowing the .d.ts degrades to any/Nothing on violation
  • Leaking internal non-exported Kotlin classes through the public surface

context