Introduction
In Single-Page-Applications (SPAs), the server usually provides endpoints (API) and does no rendering at all.
With TypeScript you can ensure the endpoints are called with the correct types. In this posting I will show how to convert message types of an API to TypeScript types automatically within the context of Spring MVC.
TypeScript lets you develop JavaScript in a typesafe mode. By that I mean it prevents you from introducing bugs due to the use of the wrong types.
Similar to header files in C, type definition files exist for libraries written in JavaScript, like jQuery, that enhance the libraries with type information
Our endpoints will have such type definitions as well. Instead of fetching them via npm , however, we will generate them dynamically.
The backend application
Given a Spring MVC application with an endpoint that returns a list of available products:
@RestController public class MainController { @RequestMapping(path = "products") public ProductsResponse getProducts() { Product foo = new Product(); foo.setAmountInStock(5); /* setting other properties */ Product bar = new Product(); bar.setAmountInStock(3); /* setting other properties */ ProductsResponse returner = new ProductsResponse(); returner.setProducts(Arrays.asList(foo, bar)); return returner; } } @Data public class BaseRequest { private Region region; } public enum Region { CENTRAL_EUROPE, SCANDINAVIA, UK } @Data public class ProductsRequest extends BaseRequest { private Instant validFrom; private Date validTo; }
We don’t query a database in our example. We just return some test data. As you can see, the ProductsRequest
has two different types for representing a date: java.time.Instant
and java.util.Date
. Furthermore, it inherits from a BaseRequest
since, in our hypothetical scenario, all requests must indicate the context in the form of its market region.
The response contains an array of the available products for that region:
@Data public class ProductsResponse { private List<Product> products; } @Data public class Product { private String code; private String groupName; private int amountInStock; private BigDecimal price; }
So next to the conversion of the default data types (String, int, boolean) the generator has to provide a solution for following cases:
- Inheritance (
ProductsRequest
/BaseRequest
) - Enumerations (
Region
) - Customized mapping (
java.time.Instant
) - Arrays
Generating the Types
The generator itself is available as a gradle or maven plugin on https://github.com/vojtechhabarta/typescript-generator. Since we are using maven in our example, we add the following plugin-section to the pom.xml:
<plugin> <groupId>cz.habarta.typescript-generator</groupId> <artifactId>typescript-generator-maven-plugin</artifactId> <version>1.25.322</version> <executions> <execution> <id>generate</id> <goals> <goal>generate</goal> </goals> </execution> </executions> <configuration> <jsonLibrary>jackson2</jsonLibrary> <classes> <class>com.hahnekamp.message.ProductsRequest</class> <class>com.hahnekamp.message.ProductsResponse</class> </classes> <customTypeMappings> <mapping>java.time.Instant:Date</mapping> </customTypeMappings> <outputFile>app/src/endpoints.d.ts</outputFile> <outputKind>global</outputKind> <namespace>endpoint</namespace> </configuration> </plugin>
What’s going on here? We define our request and response type as classes to be mapped. Although the sample code explicitly maps each class, you could use glob patterns instead. You can create customized mappings for incompatible types. We do this in our example by converting java.time.Instant
to a Date type in the customTypeMappings tag. Eventually we have to set the name of the TypeScript declaration file we will use. In our case we write it into endpoint.d.ts.
The plugin executes directly after the maven build phase “compile”. In our case, this means that each time we execute spring-boot:run we get an updated typings file. We can manually trigger the code generation by running:
./mvnw typescript-generator:generate
The generated file will look like:
declare namespace endpoint { interface ProductsRequest extends BaseRequest { validFrom: Date; validTo: Date; } interface ProductsResponse { products: Product[]; } interface BaseRequest { region: Region; } interface Product { code: string; groupName: string; amountInStock: number; price: number; } type Region = "CENTRAL_EUROPE" | "SCANDINAVIA" | "UK"; }
As you can see, the generator mapped the required classes BaseRequest
and Enumeration automatically even though we did not set in the pom.xml.
Integrating the types into our code
We can now test it by creating a minimalistic node script in TypeScript. We create an index.ts in the folder app/src where the endpoint.d.ts is already stored with the following content:
import * as request from "request"; import * as _ from "lodash"; import ProductsRequest = endpoint.ProductsRequest; import ProductsResponse = endpoint.ProductsResponse; import {error} from "util"; function requestServer(data: ProductsRequest): Promise<ProductsResponse> { return new Promise<ProductsResponse>((resolve, reject) => request.get("http://localhost:8080/products", (error, response) => { if (error) { reject(error); } else { resolve(JSON.parse(response.body)); } }) ); } let requestData: ProductsRequest = { region: "CENTRAL_EUROPE", validFrom: new Date(2017, 0, 1), validTo: new Date(2017, 1, 1) }; requestServer(requestData) .then( productsResponse => _.map(productsResponse.products) .forEach(product => console.log(product.code + " á " + product.price)), error => console.error("Error occured. Please check that the server is running") );
Please note that the whole source code is available on https://github.com/rainerhahnekamp/java-typescript-code-generator.
Final Thoughts
Your SPAs probably have domain objects you are using in TypeScript that are very similar to endpoint types. I think the best thing to do in such a situation is to stick with what you already have. Use the generated ones only for the services that are actually doing the endpoint communication.
For example, you could already have an interface for the type product that consists of code and price. You don’t even need to to use a mapping function client-side:
interface DomainProduct { code: string, price: number } let endpointProduct: Product = {code: "Nertsedus D-Class", groupName: "cars", amountInStock: 10, price: 10.53}; let product: DomainProduct = endpointProduct;
After all, the reason for generating the typings is to make sure we are calling the endpoint correctly. The structure of our endpoint should not dictate the structure of the domain models on the client side. We still strive for loose coupling.
In my example I’ve used Spring MVC that required me to indicate which classes I want to have converted. If your endpoint is running on an JAX-RS implementation like Jersey, then the generator can detect and convert the message types on its own.
You have to be aware that your IDE can’t provide full refactoring. For example, if you rename a class it would be reflected in endpoint.d.ts but all other typescript files referring to that class will fail. You have to fix them manually.
For my closing advice: Look out when using “TypeScripted” libraries like Angular. Its http service already supports requests with Generic types.
Hello,
very nice article. It’s an interesting idea having types for endpoints. But I don’t get the thing with the enums.
As I see from your example code, your java enum ‘Region’ is mapped as a string in typescript. Why is that? It basically breaks your typesafety.
Is there a way to use Typescript enums?
Hi Otto,
Java enum values are send as string values in JSON but TypeScript enums are currently based on numbers only so they can’t represent values sent in JSON.
Regarding `Region` example: more precisely it is mapped to union of string literals not a arbitrary string. This means that only specified string values can be used. So actually it is reasonably type safe.
BTW TypeScript 2.4 will allow string values in enums and there is already pull request to support this in typescript-generator in addition to current solution with string literals: https://github.com/vojtechhabarta/typescript-generator/pull/144
Hi Rainer,
thanks for this article about typescript-generator.
I also liked your advice how to deal with tight coupling when using TypeScript types generated from server classes.