Scala TSI can automatically generate Typescript Interfaces from your Scala classes.
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.8.3")
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
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>"
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 |
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 |
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[Email] = 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[Person] = 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
}
This document contains more detailed explanation of the library and usage
Currently, scala-tsi cannot always 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 will get a warning on Scala 2, and an error on Scala 3. The Scala 2 output might also not always be as desired. To fix this, you can explicitly define the right values.
object B {
// This explicit definition is to help scala-tsi with the recursive definition of A and B
private implicit val aReference: TSType[A] = TSType.external[A]("IA")
implicit val bTS: TSType[B] = TSType.fromCaseClass[B]
}
object B {
// This explicit definition is to help scala-tsi with the recursive definition of A and B
private given TSType[A] = TSType.external[A]("IA")
prviate val generatedTSType = TSType.getOrGenerate[B]
given TSType[B] = generatedTSType
}