TypeScript Data Contracts

In JavaScript (and other dynamicly typed languages) data contracts are usually implimented manually. Even when data transfer objects are automatically generated breaking changes in your code will not be found.

For example, if your data contract changes a property name from firstname to firstName, in JavaScript both are valid property names. In TypeScript only the later would be valid. The hard to spot bug would be caught by the TypeScript compiler in CI.

This is where having statically typed data contracts can save a lot of time and effort. For developers who have worked with staticly typed languages this concept will be nothing new.

This is essential if you want to be able to catch integration bugs before they get to QA or production. Even fully automated inteagration tests can't catch every bug like this that are caught a build time using data contracts.

If you are using a statically typed language (C#, Java, Scala, Rust, Go, TypeScript, etc) you should be using data contracts for your Rest or JSON/HTTP services.

Infact this concept should not just be limited to TypeScript clients. You should be using data contracts with Java and Swift clients as well.

Where to Start

Two options available for generating your data transfer objects are:

  1. Single langugage source where your client and server are written in the same langauge.
  2. Generate your data transfer objects from one language to another.
  3. Generate your client and server data transfer objects from a declarative schema language.

Single Language

This is the simplest option. You may have a Node.js backend and browser frontend both written in TypeScript. In this case you can simply consume the same TypeScript data transfer objects in both your client and server.

Languge to Languge

The second option is fairly simple as well. There are a few existing tools that will allow you to easily do this like:

  • TypeLITE that let you generate TypeScript types from C# classes.
  • ts-java that lets you generate TypeScript classes from Java jar files.

Declarative Contract

The third option is much more robust and removes tight coupling between the client and server. Instead both client and server will both consume the same data contract.

Two obvious choices for data contract language are XSD (XML Schema Definition) and JSON Schema.

Off the Shelf Tools

There are a number of tools to generate types in many languages from XSD and JSON Schema.

Custom Tooling

If the off the shelf tools don't fit your specific needs creating your own code generator is not rocket surgery.

Workflow

Let CI do much of the heavy lifting. When a breaking change is made to the data contract those changes should affect and break the builds of the client and server.

It may be asked, Why do we want to break builds? It is better to break a build rather than have runtime contract mismatches. The compiler will guide you in resolving the breaking changes.

We can import the data contracts into our client and server repositories as a git submodule. If we set fetch.recurseSubmodules to true each update will retrieve the latest changes to the data contract.

Ideally you will want to configure CI to automatically update the the Data Contract submodule in the Client and Server repositories when the Data Contract repo changes.

Once the workflow has been setup you will the flow will look something like this.

  1. Changes are made to the data types in the Data Contract repository.
  2. The Client and Server repositories are updated and the new data contract is brought in through the submodule.
  3. CI will report build errors related to the data contract breaking changes.

This simple workflow can be expanded on. You will probably want to version your contract and tie specific consumer repositories to specific contract versions.

There are numerous advantages to using a statically typed language. A statically typed data contract is of the often overlooked but very powerful tools that can easily be implimented.