A simple Java service is fairly easily developed and deployed, thanks to the overarching magic of Spring Boot. But as soon as closed classes have to be tested and data have to be transformed, a myriad of builders, mappers, enum constructors and serializers start to pave the dreadful path down into Java’s boilerplate hell. This is often a cause for delays in the development of new features. And yes, code generation is a thing, but arguably not a very lean one.
A repository containing a Node/TypeScript sample web app and some additional explanations can be found on github. There is also an advanced branch with an Onion Architecture example and some more advanced typing concepts.
First things first: TypeScript is an asynchronous, functional programming language that none the less supports classes and interfaces as well as public, private and protected attributes. This gives the programmer a good amount of flexibility in terms of microarchitecture and coding style. Its compiler can be configured dynamically i. e. to control which types of imports are allowed, if functions need explicit return types and whether to enable null safety checks at compile time.
Dependency management is easily done with the lean but powerful Node Package Manager, npm.
Class definitions support
private access control and should look fairly familiar to most developers:
The Order class now has two attributes: A private
status and a public, readonly
id field. In TypeScript, constructor arguments with a
private keyword automatically become class attributes.
Note that, because TypeScript uses type inference, a
User object can be instantiated without the need of an actual
User class. This struct-like approach is often chosen for pure data entities without any need for methods or internal state.
TypeScript expresses generics pretty much in the same way Java does:
A powerful type system
TypeScript’s type system is built on type inference and supports static typing. However, static type annotations are not necessarily required if a return or parameter type can be derived from context.
TypeScript also allows to make use of union types, partial types and type intersections, providing a lot of flexibility while avoiding needless complexity. The language also allows to use actual values as a type which is incredibly handy in a lot of situations.
Enums, type inference and union types
Consider a common situation in which an order status should have a type safe representation as an enum, but also needs a string representation for JSON serialization. The Java way to do this would be to declare an
enum together with a constructor and a getter for string values.
In a first example, TypeScript enums allow to directly add a string representation. So we would have a type-safe enum representation to work with that automatically serializes to its associated string representation.Note the last line of code, where type inference allows us to instantiate an object that matches the `Order` interface. Since our order has no need for internal state or logic, classes and constructors are not necessary.
However, using a combination of type inference and union types, there is an even easier way to solve this:
The TypeScript compiler will only accept one of the provided strings as a valid order status value (note that validation of incoming JSON data will still be necessary).
These type representations basically work with anything. A type could actually be a union type that, for example, is composed of a string literal, a number and any other custom type or interface. For more interesting examples of what can be done, have a look at TypeScript’s advanced typing tutorial.
Lambdas and functional Arguments
Being a functional programming language, TypeScript supports anonymous functions, so called lambdas, at its very core.
In the example above,
.filter() accepts a function of type
(a: T) => boolean. That function is represented by the anonymous lambda
i => i % 2 == 0.
Unlike Java, where functional parameters need to have an explicit type, a functional interface, the type of a lambda can also be represented anonymously:
async/await pattern. A promise is basically an immediately returned value that promises to return an actual value at a later time.
.then() statements can be chained indefinitely, in some cases, the pattern above could still lead to somewhat unreadable code. Declaring a function
async und using
await to wait for a promise to resolve makes it possible to write the same code in a much more synchronous-like manner. This also opens a way to use the well known
Note that, although the code above looks synchronous, it actually isn’t (hence returning another Promise).
Spread and rest to make your life easier
Using Java, data handling, constructing, merging and destructing objects tend to produce an enormous amount of boilerplate code. Classes have to be defined, constructors, getters and setter have to be generated and objects have to be instantiated. Tests cases often need to make extensive use of reflections to mock instances of closed classes.
In TypeScript, this can actually be a fun experience, using some of its sweet, typesafe, syntactic sugar: The spread and rest operators.
Let’s start by using the array spread operator
... to unpack an array:
This sure is handy, but TypeScript really takes off once you realize this also works with objects:
Let’s see what’s happening here. Basically, the
updated object is being created using the curly brackets constructor. Within that constructor, each parameter actually creates a new object, starting on the left hand side.
So the spread-out
userProfile object is used first to basically create a copy of itself. In the second step, the spread-out
update object is merged into and reassigned to the first one, again creating a new object. In the last step, the
lastUpdated field is merged and reassigned, creating a new object and final object as a result.
Using the spread operator to make immutable object copies has proven to be a very safe and fast way to handle data. Note: The object spread operator creates a shallow copy of an object. Elements with a depth greater than one will be copied as references.
The spread operator also comes with its destructive equivalent, called object rest:
At this point, it might be a good time to just lean back and think about all the Java code you would have to write to perform operations like the ones above.
Wrapping up some pros and cons
Because of its asynchronous nature and its fast runtime environment, there are many scenarios in which Node/TypeScript service can keep up with a Java service. The stack is especially well suited for I/O operations and will still work just fine for occasional, short blocking operations like resizing a new profile image. However, if the service’s main purpose is to do some CPU intensive work, Node and TypeScript are almost certainly not your best option.
The number type
Another drawback is TypeScript’s
number type that leaves integers and float values undistinguished. Practice showed, that for a lot of applications this will likely not cause any problems. Yet it also means that TypeScript shouldn’t be your first choice for a bank account or checkout service.
Where to start
Amongst many others, there are some easy to use, well working libraries, i. e. the popular, low level expressJS web server. A solid choice for a full web framework is nestJS. It uses the expressJS web server under the hood and offers lots of abstractions and solutions for common challenges like route guards, dependency injection, declarative controller definitions, inter service communication and a lot more. Its architecture should also be easily recognizable for developers familiar with AngularJS or Java Spring Boot.
But if a service will heavily depend on a lot of complex and well maintained third party software, Python, Java or Clojure might be a better choice.
Faster feature development
As we have seen above, one of TypeScript’s biggest advantages is its ability to express complex logic, concepts and operations in a lean and simple manner. The fact that JSON is an inherent language feature and nowadays a widely used data serialization format for data transfer and document based databases often makes the language feel like a natural choice. A Node server setup is quickly done, mostly without unnecessary dependencies and will spare your systems resources. That’s why Node.js in combination with TypeScript’s strong type system can effectively enable a team to deliver new features in little time.
Last but not least, a good chunk of syntactic sugar makes TypeScript development a fun experience: In my experience, motivated teams work harder and faster.
Photo by Vinicius Amano on Unsplash