02 April 2024, Joachim Schirrmacher

My journey from TypeScript to Java - Part 1

My journey from TypeScript to Java - Part 1

For several years now, I’ve been programming in Typescript and enjoyed it significantly due to its simple and yet powerful type safety system in contrast to JavaScript.

However, a lot of people in DB Systel use Java as their preferred programming language. To understand what they like about Java, but also because it is easier to maintain the software in the team, I wanted to learn Java myself.

My learnings on my way from TypeScript to Java might be useful for others going the same way. While I’m going this way, I like to add more of these articles. For now, I start with some beginner problems, but I already have prepared some more!

Many thanks to my friends who helped me with the formulation of my travelogue and showed me new ways:

No some objects are equal

The first thing I stumbled upon might be a real beginner problem. I expected that I could compare strings very similar to TypeScript with the equality operator. Sure, the === operator in TypeScript is very special, but the IDE stops you from using it in Java.

However, it doesn’t stops you to use the 'normal' == equality operator for strings. Since String is a class, and classes are only equal, if they are the same object, equality on two Strings normally doesn’t work. You might already know this behavior from TypeScript when, e.g. comparing `Date`s.

To make things more complicated, there are situations where comparisons work nevertheless:

String param = "expected";
if (param == "expected") {
  System.out.println("These strings are equal!")
}

This happens because Java re-uses string literals like 'expected' in this example. So, the param object and the "expected" string literal are actually equal!

However, if the String is created dynamically (e.g. from user input), it doesn’t:

String param = "  expected  ".trim();
if (param != "expected") {
  System.out.println("These strings are not equal, even if they have the same content!")
}

In Java, comparing of objects is done with an equals() method:

if (param.equals("expected")) {
  System.out.println("Now the equality is recognized")
}

However, on primitive types, equality is implemented just like I would expect. So a == 5 works if a is an int. And, surprisingly, if you see a variable defined as Integer a, you can use a == 5, though Integer is a class and a therefore is an object of this class. Actually, the == operator defined here overloads the standard one, so the 'normal' rule of non-equality of objects if they are not the same is not applicable.

Try it out yourself:

class Test {
  public static void main(String[] args) {
    Integer a = Integer.parseInt(args[0]);

    if (a == 5) {
      System.out.println("a is 5");
    } else {
      System.out.println("a is not 5");
    }
  }
}

In my opinion, this is not very intuitive, but that might be due to my past in TypeScript. Regrettably, Java implements function overloading only in a few cases (like the above) and doesn’t give us the opportunity to define them ourselves. Else, I would create a MyString class and define a == method to compare strings like numbers.

Fun with null values

Typescript inherits the sometimes difficult distinction between undefined and null from Javascript. Some people might think, this is not really necessary, but the two actually have a slightly different meaning: while null means that I actually assigned a kind of empty value to a variable or parameter, undefined means 'not set'. null is rather empty on purpose.

Java doesn’t make such a distinction, it only has null - which might be sufficient for most use cases.

But how does both languages handle such values?

Let’s say, you have a variable myObject which might be an object having a function value() but also may be undefined. The function might return a Date or again undefined. How can I compare this with the current time?

In Typescript this can be done with the so called 'Optional Chaining' like this:

if (myObject?.value()?.getTime() > Date.now()) {
  // ...
}

The ? means that when we get an undefined, just stop evaluating, returning undefined as the result of the expression. TypeScript programmers know this as 'falsy', which means that it something that is similar to the implementation of false. Falsy in TypeScript are the values 0, undefined, null, "" (the empty string), and, of course, false. This is a very compact and, if you understand the syntax, readable form.

In Java, there is no such thing as a optional chaining ?. operator, we have to check every possible null value. Also, Java uses different types for numbers, int or long, so we need to decide, what value() should return. If we want to use null as well, both types won’t allow that.

But there is an alternative: instead of using the native int or long types, use the classes Integer or Long (with capital first letter) for the return value of value() instead! They may be null (as all objects).

if (myObject != null
  && myObject.value() != null
  && myObject.value() > Long.valueOf(Instant.now().getEpochSecond())) {
    // ...
}

Why the hell do I need to specify types?

As you saw in my previous examples, Java requires you to specify the data type when defining a variable. While in Typescript, a definition with a value looks like

const string = functionThatReturnsAString();

in Java, it requires an additional specification

String string = functionThatReturnsAString();

This gets particularly strange, if you have a value that needs to be explicitly converted to a string:

String string = functionThatReturnsAnObject().toString();

When the function is already specifying a return type, specifying the type of the variable seems to be just overhead. The compiler could just infer the type automatically!

But to improve the situation in Java 10 and higher, instead of the data type, one can use the var keyword (which is rather a reserved type name, to be exact), so that it looks similar to TypeScript:

final var string = functionThatReturnsAnObject().toString();

The Java compiler then also automatically infers the actual type.

In fact, TypeScript also has a var keyword, though I never would use it, and instead only use const or - in rare cases - let. Differentiating between variables that can change later (let) and those which may not be changed (const - an immutable value) is a very useful feature to avoid unwanted changes.

Union types

A nice feature of TypeScript are Union Types. They allow to combine multiple types to be used in a clear way:

type Fruit = "apple" | "orange" | "banana";
type DairyProducts = "milk" | "butter";

type Food = Fruit | DairyProducts;

There is no such thing as Union Types in Java. In Java, you would instead use enum`s for defining `Fruit and DairyProducts.

@AllArgsConstructor
public enum Fruit {
  APPLE("apple"),
  ORANGE("orange"),
  BANANA("banana");

  String name;
}

The upper case identifiers work as constants, the string literals in the braces the values of these constants. The field name is needed to hold the value in each of the three instances of Fruit. Fruit values can be accessed like this:

Fruit fruit = Fruit.BANANA;

However, it is not easily possible to combine such enums, because they are compiled to constants.

Instead, you would use interfaces and let the Fruit and the DairyProduct enums implement this interface.

public interface Food {}

@AllArgsConstructor
@Getter
public enum Fruit implements Food {
  APPLE("apple"),
  ORANGE("orange"),
  BANANA("banana");

  String name;
}

@AllArgsConstructor
@Getter
public enum DairyProduct implements Food {
  MILK("milk"),
  CHEESE("cheese");

  String name;
}

Now you can use the new Food interface as the type to collect both, `Fruit`s and `DairyProduct`s together:

List<Food> food = List.of(Fruit.BANANA, DairyProduct.CHEESE);

Note that it is not possible (as far as I know) to use Food as the qualifier for BANANA and CHEESE and that it is not that easy to use the lower case equivalents for assignments. Instead, one would need to iterate over the enum values and find the requested value text.

I use Lombok to not have to implement getters and constructors. They are necessary to have the lower case values at least when using the enum for writing to a database or when generating JSON.

Also note that every interface and class needs a separate file in Java.

All in all, `enum`s seems to be a very cumbersome feature. But this is also valid for TypeScript, where I prefer to use Union Types.

Combine object structures

To combine object structures in TypeScript you would use the &:

type Person = {
  name: string;
  email: string;
};

type AuthDetails = {
  username: string;
  password: string;
};

type User = Person & AuthDetails;

In Java, you would rather define two interfaces, and define a class implementing both. This would work in Typescript as well. However, interfaces in TypeScript can only be used to describe object strucures, not primitives. Read more about the differences of type aliases and interfaces in the official TypeScript documentation.

I’m still working with Java, so stay tuned to read more experiences in the next chapters of my journey, when I will cover optional values, defaults and mapping of objects.

typescript java