Skip to content

Change approach for generating TypeScript types #84

Description

@tajakobsen

Code generation approach for TypeScript types

Proposal

Generate the TypeScript types with a small, indentation-aware CodeWriter written in Kotlin, instead of using a third-party
code generation library or template engine.

Background

The generated code only uses a few TypeScript constructs:

  • export type X = { ... }; with object types
  • Unions of string literals, and Array<...>
  • JSDoc comments
  • import("...") type references
  • declare global { ... } for the global maps

The current renderers build this with string templates and trimMargin, and they pass the indentation level through
every function. Most of the complexity comes from managing whitespace and indentation, not from TypeScript itself.

The snapshot tests in src/test/snapshots require the output to stay byte for byte the same after the rewrite. That
makes full control over formatting a hard requirement.

Suggested design

A small writer that keeps track of indentation, so renderers never compute indentation themselves:

class CodeWriter {
  private val out = StringBuilder()
  private var level = 0

  fun line(text: String = "") = apply {
    out.append(if (text.isEmpty()) "" else "  ".repeat(level) + text).append('\n')
  }

  fun block(open: String, close: String, body: CodeWriter.() -> Unit) = apply {
    line(open); level++; body(); level--; line(close)
  }

  override fun toString() = out.toString()
}

Small helpers on top of it, e.g. comment(), property(), objectType() and union(), can each render one construct.
Nested item sets and option sets then just call block() recursively.

Why not a library?

  • TypeScript generators from JVM classes (typescript-generator,
    kotlinx-serialization-typescript-generator,
    ts-generator, jvm2dts) generate types
    from existing Java/Kotlin classes. Our types are only known at runtime, from the XP schemas, so these don't fit.
  • typescriptpoet is a builder API for TypeScript, but its last release
    was in 2021. It also formats the output its own way, which would make it hard to keep the output unchanged.
  • Template engines (StringTemplate 4, JTE,
    Pebble, JMustache) would work.
    They add a dependency, though, and JTE, Pebble and JMustache leave indentation of nested structures to us, which is the
    part we want to simplify. StringTemplate 4 does indent nested templates, but it hasn't had a release since 2022.

Benefits

  • Full control over the output, so the snapshot tests keep passing through the rewrite.
  • No extra dependency. The plugin runs inside users' Gradle builds, where every dependency risks conflicting with
    other plugins.
  • Simpler renderers. No trimMargin, and no indentation levels passed around.
  • Easy to unit test, since the writer is independent of the XML/YAML parsing.

If we later need something the writer can't handle well, StringTemplate 4 or JTE are the fallback options.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions