skip to content

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%

answer

  1. .d.ts = the published contract; guard it in CI
  2. Thin exported facade over rich internals
  3. Exportable types only; convert at the edge
  4. @JsName for names + overloads; avoid default args
  5. Renames/removals = breaking changes

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.

solid answer

~50 s

Treat the `@JsExport` surface as a **published contract**, not an afterthought. Principles: (1) Expose a **thin facade** — a few exported classes/functions/objects — over rich internal Kotlin; never leak non-exported types. (2) Use only **exportable types** at the boundary; convert collections to `Array`, `Long` to `String`/`Double`, and prefer small exported DTO classes over Kotlin data classes with non-exportable members. (3) Use **@JsName** to give JS-idiomatic names and to project overloads onto distinct names; avoid default arguments (they synthesize overloads). (4) Enable **TypeScript declaration generation** and **review the `.d.ts` in CI** as a golden file so accidental signature changes are caught. (5) Version the surface carefully: renames/removals are breaking changes for TS consumers. (6) Prefer plain value-shaped results and explicit error signaling that maps to JS, since Kotlin exceptions cross to JS as thrown `Error`s. The goal is a `.d.ts` that reads like a hand-written, idiomatic TypeScript API.

code

kotlin · 13 lines
kotlin
internal data class Order(val id: Long, val lines: List<String>)

@JsExport
class OrderView(val id: String, val lineCount: Int)

@JsExport
class OrderApi(private val repo: Repo) {
    @JsName("load")
    fun load(id: String): OrderView {
        val o = repo.find(id.toLong())
        return OrderView(o.id.toString(), o.lines.size)
    }
}

go deeper

for a junior

Knows to keep exported types simple and JS-friendly.

for a middle

Builds a facade and converts collections/Long at the edge.

for a senior

Adds @JsName policy, avoids default args, and reasons about exception/async mapping to JS.

for a principal

Governs the .d.ts as a versioned contract (golden snapshot in CI, additive evolution) and sets org-wide export-surface design rules.

## Mindset: the .d.ts is a contract A Kotlin/JS library's real public API is the generated **TypeScript declaration file** (`.d.ts`). TypeScript consumers compile against it. So design it deliberately and guard it like any published API. ## 1. Thin facade over rich internals Keep your domain logic in normal Kotlin (sealed hierarchies, `Long`, `List`, coroutines). Expose a **small** set of `@JsExport` declarations that translate to/from JS-shaped types. ```kotlin // internal rich model — NOT exported internal data class Order(val id: Long, val lines: List<Line>) // exported facade @JsExport class OrderApi { fun load(id: String): OrderView = repo.find(id.toLong()).toView() } @JsExport class OrderView(val id: String, val total: Double, val lineCount: Int) ``` Never let a non-exported type appear in an exported signature — it breaks the `.d.ts`. ## 2. Exportable types only, converted at the edge - `List`/`Set`/`Map` → `Array` (`toTypedArray()`), or a small exported wrapper. - `Long` → `String` (exact) or `Double` (lossy). - Enums export, but consider exposing `String` constants if TS ergonomics matter. - Nullable maps to `T | null`. ## 3. Names and overloads - `@JsName` for JS-idiomatic names and to split overloads (JS has no overloading). - **Avoid default arguments** across the boundary — Kotlin synthesizes overloads for them, polluting the `.d.ts`. Use nullable params or an options class. ## 4. Generate and guard the .d.ts - Turn on TS declaration generation on the JS target. - Commit the generated `.d.ts` (or a snapshot) and **diff it in CI** as a golden file so any signature drift is reviewed intentionally. ```ts // golden snapshot reviewed in CI export class OrderApi { load(id: string): OrderView } export class OrderView { readonly id: string; readonly total: number; readonly lineCount: number } ``` ## 5. Versioning discipline Renaming an exported member or changing a signature is a **breaking change** for every TS consumer. Add, don't mutate; deprecate before removing. ## 6. Errors and async cross the boundary - Kotlin **exceptions** surface to JS as thrown `Error`s — document them or return result-shaped objects for predictability. - For async, expose JS-friendly results; `Promise`-returning APIs are cleaner for TS than leaking Kotlin coroutine machinery. (Coroutine↔Promise bridging detail belongs to broader JS-interop topics, but the API-design point stands: hand JS a `Promise`-like contract.) ## Outcome A `.d.ts` indistinguishable from a thoughtfully hand-written TypeScript API: small, fully typed, stable, and JS-idiomatic — with all Kotlin richness safely behind the facade.

  • How do you prevent accidental breaking changes to the TypeScript surface?
    Generate the .d.ts, commit a golden snapshot, and fail CI on any unexpected diff so signature changes are reviewed and versioned deliberately.
  • Why avoid Kotlin data classes directly at the export boundary?
    They often contain non-exportable members (Long, List) and generated members that don't map cleanly; small purpose-built exported DTOs give an honest, stable .d.ts.

Design the export surface like an embassy: a small, well-documented front desk (facade) handling foreign visitors, with all the messy internal bureaucracy kept out of sight.

saying these in an interview costs you the question

  • Exporting the whole rich domain model directly
  • Leaking non-exported types or Long/List into signatures
  • Relying on default arguments across the boundary
  • Renaming exported members casually without versioning
  • Not reviewing the generated .d.ts as a contract

context