Skip to content

Instantly share code, notes, and snippets.

@lppedd
Last active May 25, 2025 18:52
Show Gist options
  • Save lppedd/9e72a5e35b7b698d71149297f47e68fa to your computer and use it in GitHub Desktop.
Save lppedd/9e72a5e35b7b698d71149297f47e68fa to your computer and use it in GitHub Desktop.

The problem

Let's say I have the following architecture:

image

On the TypeScript extension code I invoke a @JsExport-ed service function:

async refreshWebApp(): void {
  const data = await kotlinService.getData(); // returns Promise<Data>
  // TODO: send data to web app
}

Where data is:

// commonMain
@JsExport
class Data(val name: String, val age: Int, val addresses: List<String>)

How would I now send the data instance to the web app? I cannot simply do:

webview.postMessage(data); // Does not work!

because the returned Kotlin object uses non-serializable accessors and Kotlin-specific classes (KtList in this case).

The current solution

Well, the only way to provide a pleasant experience to our TS consumers is to also add a mapping layer.
We provide a new TS type, with accompaining mapping functions:

export type JsData = {
  readonly name: string;
  readonly age: number;
  readonly addresses: string[];
};

export function ktDataToJsData(value: Data): JsData {
  return {
    name: value.name,
    age: value.age,
    addresses: value.addresses.asJsReadonlyArrayView().slice(),
  };
}

export function jsDataToKtData(value: JsData): Data {
  return new Data(value.name, value.age, KtList.fromJsArray(value.addresses));
}

The above example then becomes:

async refreshWebApp(): void {
  const data = await kotlinService.getData();
  const jsData = ktDataToJsData(data); // Our manually coded mapping function
  webview.postMessage(jsData);
}

But imagine having to do this for dozens of exported Kotlin classes, it's difficult to justify in terms of time and maintainability.

The proposed solution

Much like what @JsPlainObject does to an external interface, a new @JsSerializable annotation applied to an exported Kotlin class could contribute static mapping functions via a companion object. In practice this would mean scrapping the manually coded type and function(s).

Simply add the annotation to the Kotlin class:

// commonMain
@JsExport
@JsSerializable
class Data(val name: String, val age: Int, val addresses: List<String>)

And again, the above example then becomes:

async refreshWebApp(): void {
  const data = await kotlinService.getData();
  const jsData = Data.toJso(data); // Mapping function generated by the compiler plugin
  webview.postMessage(jsData); // jsData is an interface JsoData, generated and exported by the compiler plugin
}

The same concept also applies to the trip back from the Web App to the VS Code extension:

async onWebAppMessage(message: JsoData): void {
  const data = Data.fromJso(message); // Mapping function generated by the compiler plugin
  await kotlinService.persistData(data);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment