An Introduction to TypeScript for Backend Development (and Java Developers)

Java is still a language of choice for backend development. And for many reasons: It’s considered fast, secure (except for its null pointers, of course) and it comes with a particularly broad, well maintained and tested ecosystem. But in an era of microservices and agility, other considerations gained importance: For some systems, peak performance might not be as crucial and a rich ecosystem of stable dependencies might not be needed for a service that just provides some CRUD operations and data transformation. More, smaller systems might have to be built and rebuilt in shorter time to stay on track with rapid, iterative feature development.

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.

TypeScript doesn’t seem to have a good reputation amongst backend developers. Probably because it’s mostly known to be a set of declaration files to add some typing to JavaScript. Yet, a lot of logic that would require dozens of lines of Java code can be handled using just a few lines of TypeScript.

A lot of the features that will be mentioned here as belonging to TypeScript are actually part of JavaScript. However, TypeScript can also be seen as a standalone language that happens to share some syntax and concepts with JavaScript. So let’s forget about JavaScript for a minute and have a look at TypeScript on its own: An elegant language with an extremely powerful, yet flexible type system, lots of syntactic sugar and - last but not least - null safety!

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.

Introducing TypeScript

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.

Since TypeScript compiles down to plain JavaScript, Node.js is used as a backend runtime environment. From the absence of an all-embracing Spring-like framework follows that a typical web service will use a leaner web server framework (like the excellent Express.js) and therefore look less magical and more explicit in its basic setup and configuration. This means that some more complex services might take a little more setup effort. On the other hand, the setup of smaller applications is done easily and without a lot of prior framework knowledge.

Dependency management is easily done with the lean but powerful Node Package Manager, npm.

The basics

Class definitions support public, protected and private access control and should look fairly familiar to most developers:

class Order {

    private status: OrderStatus;

    constructor(public readonly id: string, isSpecialOrder: boolean) {
        [...]
    }
}

The Order class now has two attributes: A private status and a public, readonly id field. In TypeScript, constructor arguments with a public, protected or private keyword automatically become class attributes.

interface User {
    id?: string;
    name: string;
    t_registered: Date;
}

const user: User = { name: 'Bob', t_registered: new Date() };

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:

class Repository<T extends StoredEntity> {
    findOneById(id: string): T {
        [...]
    }
}

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.

enum Status {
    ORDER_RECEIVED = 'order_received',
    PAYMENT_RECEIVED = 'payment_received',
    DELIVERED = 'delivered',
}

interface Order {
    status: Status;
}

const order: Order = { status: Status.ORDER_RECEIVED };
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:

interface Order {
    status: 'order_received' | 'payment_received' | 'delivered';
}

const orderA: Order = { status: 'order_received' }; // will compile
const orderB: Order = { status: 'new' }; // will NOT compile

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.

const evenNumbers = [ 1, 2, 3, 4, 5, 6 ].filter(i => i % 2 == 0);

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:

class OrderService {
    constructor(callback: (order: Order) => void) {
        [...]
    }
}

Async programming

Since TypeScript is, after all, a superset of JavaScript, asynchronous programming is a core concept. Although, of course, lambdas and callbacks could be used,TypeScript has essentially two ways to avoid callback hell: Promises and the beautiful async/await pattern. A promise is basically an immediately returned value that promises to return an actual value at a later time.

// an asynchronous function returning a promise
function fetchUserProfiles(url: string): Promise<UserProfile[]> {
    [...]
}

// could either be used like this
function getActiveProfiles(): Promise<UserProfile[]> {
    return fetchUserProfiles(URL)
        .then(profiles => profiles.filter(profile => profile.active))
        .catch(error => handleError(error));
}

Because .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 try/catch statements:

// using async/await (throws an error if fetchUserProfiles throws an error)
async function getActiveProfiles(): Promise<UserProfile[]> {
    const allProfiles = await fetchUserProfiles(URL);
    return allProfiles.filter(profile => profile.active);
}

// or with try/catch
async function getActiveProfilesSafe(): Promise<UserProfile[]> {
    try {
        const allProfiles = await fetchUserProfiles(URL);
        return allProfiles.filter(profile => profile.active);
    } catch (error) {
        handleError(error);
        return [];
    }
}

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:

const a = [ 'a', 'b', 'c' ];
const b = [ 'd', 'e' ];

const result = [ ...a, ...b, 'f' ];
console.log(result);

// >> [ 'a', 'b', 'c', 'd', 'e', f' ]

This sure is handy, but TypeScript really takes off once you realize this also works with objects:

interface UserProfile {
    userId: string;
    name: string;
    email: string;
    lastUpdated?: Date;
}

interface UserProfileUpdate {
    name?: string;
    email?: string;
}

const userProfile: UserProfile = { userId: 'abc', name: 'Bob', email: 'bob@example.com' };
const update: UserProfileUpdate = { email: 'bob@example.com' };

const updated: UserProfile = { ...userProfile, ...update, lastUpdated: new Date() };

console.log(updated);

// >> { userId: 'abc', name: 'Bob', email: 'bob@example.com', lastUpdated: 2019-12-19T16:09:45.174Z}

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:

const userProfile: UserProfile = { userId: 'abc', name: 'Bob', email: 'bob@example.com' };
const { userId, ...details } = userProfile;
console.log(userId);
console.log(details);

// >> 'abc'
// >> { name: 'Bob', email: 'bob@example.com' }

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 pro’s and con’s

Performance

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.

The ecosystem

Due to Node.js' popularity, there are now hundreds of thousands of packages out there. But because Node hasn’t been around as long as Java, version numbers are often low and the code quality of some libraries leaves room for improvement. Since TypeScript still hasn’t fully arrived as a first class citizen in the Node.js universe, there are still very few JavaScript libraries left without typings.

Amongst many others, there are some easy to use, well working libraries, i. e. for web servers, dependency injection and controller annotations. 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.

Links:

Photo by Vinicius Amano on Unsplash

TAGS

Comments

Please accept our cookie agreement to see full comments functionality. Read more