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?
answer
- external = typed facade, dynamic = escape hatch
- external: compile-time errors; dynamic: runtime errors
- @JsModule/@JsName bind external to npm exports
- Hybrid: external hot path, dynamic at the edge
- Narrow dynamic with unsafeCast<T>() ASAP
basics
~20 sUse 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.
solid answer
~40 sDefault to `external`: you write typed Kotlin facades (`external class`/`interface`/`fun`, usually with `@JsModule`/`@JsName` to bind the npm export) so the compiler verifies call sites, gives IDE help, and enforces null-safety. The cost is up-front authoring and keeping facades in sync with the library's API. Reach for `dynamic` when the API is sprawling, loosely typed, returns free-form JSON, or you're prototyping — it eliminates facade work but disables all checks, so typos become runtime `TypeError`s and there's no autocomplete. A pragmatic hybrid: write `external` declarations for the parts you use a lot, and confine `dynamic` to a narrow boundary, narrowing back to typed values with `unsafeCast<T>()` as soon as you trust the shape. Treat broad `dynamic` as tech debt to be replaced by facades.
code
kotlin · 11 lines// Typed facade for the part you use often
@JsModule("date-fns")
external object dateFns {
fun format(date: Date, pattern: String): String
}
// dynamic only at a narrow boundary, then narrowed
fun readConfig(raw: dynamic): Config =
raw.unsafeCast<Config>()
external interface Config { val retries: Int }go deeper
Knows external is typed and dynamic is untyped, and prefers the typed option.
Explains @JsModule/@JsName binding, the effort-vs-safety tradeoff, and runtime-vs-compile failure modes.
Designs the hybrid: external hot path, narrow dynamic boundary, unsafeCast narrowing, manages facade drift.
Sets org policy and tooling for bindings, weighs maintenance cost of facades vs codegen, and treats broad dynamic as tracked debt.
## The decision Both are interop tools, but they sit at opposite ends of a safety spectrum: - **`external`** = you *declare the types*. The compiler keeps checking your calls. Maximum safety, some authoring cost. - **`dynamic`** = you *opt out of types*. Zero authoring cost, zero safety; errors surface only at runtime in JS. ## When `external` wins - The library has a stable, knowable API you call repeatedly. - You want IDE autocomplete, refactoring support, and null-safety. - You're building a shared binding others on the team will reuse. ```kotlin @JsModule("lodash") external object lodash { fun chunk(array: Array<Int>, size: Int): Array<Array<Int>> } ``` The compiler now checks every `lodash.chunk(...)` call. (`@JsModule` ties the declaration to the npm module; `@JsName` maps differing names.) ## When `dynamic` wins - The data is genuinely free-form (arbitrary JSON, a config bag). - The API is huge and you touch a tiny, shifting slice — facades aren't worth it. - You're prototyping and want to move fast. ```kotlin val result: dynamic = someLib.doWhatever() console.log(result.some.nested.field) // no checks ``` ## Tradeoffs table (in prose) - **Safety:** `external` full; `dynamic` none. - **Authoring effort:** `external` high; `dynamic` near zero. - **IDE help / refactor:** `external` yes; `dynamic` no. - **Failure mode:** `external` at compile time; `dynamic` at runtime (`TypeError`). - **Drift risk:** `external` facade can lag the real API (silent mismatch); `dynamic` never lies but never helps. ## The idiomatic hybrid Write `external` for the hot path; allow `dynamic` only at the edge, then **narrow immediately**: ```kotlin external interface Config { val retries: Int } val cfg: Config = rawDynamic.unsafeCast<Config>() ``` `unsafeCast<T>()` reinterprets the value with no runtime check, leaving `dynamic` behind. Keep `dynamic` surfaces small and documented so a typo can't propagate deep into the app. Track widespread `dynamic` usage as debt and replace it with facades over time.
- What annotation binds an `external` declaration to a specific npm module's default export?`@JsModule("<module-name>")` (paired with `@JsNonModule` if you also target non-module builds); `@JsName` maps a Kotlin name to a differing JS name.
- How do you stop `dynamic` from spreading through your codebase?Confine it to a thin boundary and immediately convert to typed values (e.g. `unsafeCast<T>()` into an `external interface`), keeping the rest of the code typed.
saying these in an interview costs you the question
- Reaching for `dynamic` everywhere to avoid writing facades
- Not knowing `@JsModule`/`@JsName` exist for binding externals
- Claiming `external` validates against the real library at runtime
- Letting `dynamic` propagate instead of narrowing it
- Ignoring facade drift as a silent-mismatch risk