scala-tsi / scala-tsi

Generate typescript interfaces from your scala classes

Version Matrix

Scala-TSI

CircleCI

2.12 2.13

Scala TSI can automatically generate Typescript Interfaces from your Scala classes.

Installation

To use the project add the SBT plugin dependency in project/plugins.sbt:

// See badge above for latest version number
addSbtPlugin("com.scalatsi" % "sbt-scala-tsi" % "0.5.0")

And configure the plugin in your project:

// Replace with your project definition
lazy val root = (project in file("."))
    .enablePlugins(ScalaTsiPlugin)
    .settings(
      // The classes that you want to generate typescript interfaces for
      typescriptExports := Seq("MyClass"),
      // The output file which will contain the typescript interfaces
      typescriptOutputFile := baseDirectory.value / "model.ts",
      // Include the package(s) of the classes here
      // Optionally import your own TSType implicits to override default default generated
      typescriptGenerationImports := Seq("mymodel._", "MyTypescript._")
    )

Now sbt generateTypescript will transform a file like

case class MyClass(foo: String, bar: Int)

Into a typescript interface like

export interface IMyClass {
  foo: string
  bar: number
}

See #Example or the example project for more a more examples

Without sbt plugin

The sbt plugin will add the required dependency to your project. It can be added manually if you don't use the plugin:

libraryDependencies += "com.scalatsi" %% "scala-tsi" % "<version>"

Configuration

Key Type Default Description
typescriptExports Seq[String] Seq() A list of all your (top-level) classes that you want to generate interfaces for
typescriptGenerationImports Seq[String] Seq() A list of all imports. This should import all classes you defined above, as well as custom TSType implicits
typescriptOutputFile File target/scala-tsi.ts The output file with generated typescript interfaces
typescriptStyleSemicolons Boolean false Whether to add semicolons to the exported model (experimental)
typescriptHeader Option[String] Some("...") A header for the output file. Contains a notice about the file being generated by default
typescriptTaggedUnionDiscriminator Option[String] Some("type") The discriminator field for tagged unions, or None to disable tagged unions

Example

You can check out the example project for a complete set-up and more examples.

Say we have the following JSON:

{
   "name": "person name",
   "email": "abc@example.org",
   "age": 25,
   "job": {
      "tasks": ["Be in the office", "Drink coffee"],
      "boss": "Johnson"
   }
}

Generated from this Scala domain model:

package myproject

case class Person(
  name: String,
  email: Email,
  age: Option[Int],
  // for privacy reasons, we do not put this social security number in the JSON
  ssn: Option[Int],
  job: Job
)
// This type will get erased when serializing to JSON, only the string remains
case class Email(address: String)

case class Job(tasks: Seq[String], boss: String)

With Typescript, your frontend can know what data is available in what format. However, keeping the Typescript definitions in sync with your scala classes is a pain and error-prone. scala-tsi solves that.

First we define the mapping as follows

package myproject

import com.scalatsi._
import com.scalatsi.dsl._

// A TSType[T] is what tells scala-tsi how to convert your type T into typescript
// MyModelTSTypes contains all TSType[_]'s for your model
// You can also spread these throughout your codebase, for example in the same place where your JSON (de)serializers
object MyModelTSTypes {
 
  // Tell scala-tsi to use the typescript type of string whenever we have an Email type
  // Alternatively, TSType.alias[Email, String] will create a `type Email = string` entry in the typescript file
  implicit val tsEmail = TSType.sameAs[Email, String]
  
  // TSType.fromCaseClass will convert your case class to a typescript definition
  // `- ssn` indicated the ssn field should be removed
  implicit val tsPerson = TSType.fromCaseClass[Person] - "ssn"
}

And in your build.sbt configure the sbt plugin to output your class:

lazy val root = (project in file("."))
  .settings(
    typescriptExports           := Seq("Person"),
    typescriptGenerationImports := Seq("myproject._", "MyModelTSTypes._"),
    typescriptOutputFile        := baseDirectory.value / "model.ts"
  )

this will generate in your project root a model.ts:

export interface IPerson {
  name : string
  email : string
  age ?: number
  job: IJob
}

export interface IJob {
  tasks: string[]
  boss: string
}

Usage

This document contains more detailed explanation of the library and usage

Circular references

Currently, scala-tsi cannot handle circular references. You will get an error along the following lines:

[error] Circular reference encountered while searching for TSType[B]
[error] Please break the cycle by locally defining an implicit TSType like so:
[error] implicit val tsType...: TSType[...] = {
[error]   implicit val tsA: TSType[B] = TSType.external("IB") // name of your "B" typescript type here
[error]   TSType.getOrGenerate[...]
[error] }
[error] for more help see https://github.com/scala-tsi/scala-tsi#circular-references

To help scala-tsi and break the cycle you will need to define an explicit manual reference. For example, if you have the following classes

case class A(b: B)
case class B(a: A)

you have to define a local implicit for one of the types, so the loop gets broken

object B {
  // This explicit definition is to help scala-tsi with the recursive definition of A and B
  implicit val bTSI: TSIType[B] = {
    implicit val aReference: TSType[A] = TSType.external[A]("IA")
    TSType.fromCaseClass[B]
  }
}

There are rare cases where this error might be false. If you are sure of this, please file a bug report.