14 language features in TypeScript and Dart you may miss in Java

Oleg Varaksin
14 min readSep 8, 2020

--

As a full-stack developer, I have to switch between several programming languages in my day-to-day business: Java (backend), TypeScript (web dev) and Dart (mobile dev). In this blog post, I will try to demonstrate fourteen built-in language constructs in TypeScript and Dart which convinced me to believe that these languages are more sophisticated and flexible than Java. You can already see in the picture above how fluent you can build collections in Dart using conditionals and repetition. These features are called “collection for” and “collection if”. Neat? But there are more. As you could also see, the type information in Dart is preserved at runtime. The check if the variable listOfStrings has the type List<String> works fine at runtime. In contrast, Java has type erasure, which means that generic type parameters are removed at runtime. In Java, you can’t test directly whether a collection is of type List<String>.

After reading this blog post, you will notice that people behind TypeScript and Dart spent much time in language design to make them user-friendly. In this article, we will cover:

  • String interpolation
  • Multiline strings
  • Parameter properties in constructor
  • Optional parameters
  • Default parameter values
  • Null-aware operators
  • Spread operator
  • Indistinguishability of fields and getters / setters
  • Mixins (aka multiple inheritance)
  • Dynamic extensions of existing types
  • Asynchronous programming with async / await
  • Generators
  • Control flow analysis
  • Method chaining (amazing builder pattern!)

You can try all code snippets in TypeScript Playground or DartPad respectively.

1. String interpolation

String interpolation is a process of evaluating the final string by injecting a variable or an expression in a string literal.

TypeScript

The string interpolation is an ECMAScript 2015 (ES6) feature. It works by using${variable} or ${expression} syntax. An example:

Dart

In Dart, you can use $variable or ${variable} syntax for the string interpolation. The variable itself can be of any type. The expression in ${} can be used as well.

Dart internally calls toString() on objects that are being interpolated.

Sure, in Java you can leverage String.format(...) but built-in language constructs in TypeScript and Dart are shorter and more concise.

2. Multiline strings

TypeScript

Multiline strings is an ES6 feature. They can be created with a backtick (`) at begin and end of a string. No + sign is necessary for string concatenation.

Dart

The Dart’s way to create a multiline string is a triple quote with either single or double quotation marks.

Furthermore, if you have two string literals, you do not need to use + to concatenate them. Example:

3. Parameter properties in constructors

Parameter properties in constructors let you create and initialize a class member in one place.

TypeScript

Parameter properties are declared by prefixing a constructor parameter with an accessibility modifier or readonly, or both. For example, instead of

just write

Dart

In Dart, this feature is called “initializing formals”. Just use the this. syntax before a constructor parameter. The main rule of effective Dart is: do use initializing formals when possible.

Note that in Dart you can not omit the property declaration (see the code snippet above). But in Dart you can use ; instead empty constructor body{}.

Saidy, but this simple and handy feature is missing completely in Java.

4. Optional parameters

Optional parameters are optional in that the caller isn’t required to specify a value for the parameter when calling a function.

TypeScript

In TypeScript, we can specify that a property (in interfaces / classes) or a parameter (in functions) is optional with a question mark (?) after the name. Optional parameters are especially handy in functions. An example:

Dart

Optional parameters in Dart can be named and positional. A parameter wrapped by [ ] is a positional optional parameter. A parameter wrapped by { } is a named optional parameter. Named parameters can be referenced by names when invoking a function. Values for named parameters can be passed in any order, when invoking the function (this is the main benefit of them). The required parameters are listed first, followed by any optional parameters.

5. Default parameter values

An optional parameter can have a default value which is used when a caller does not specify a value. Such default values are provided after the = operator.

TypeScript

The TypeScript example above can be rewritten now as

The function invocation at the default value’s position can be placed as well. By this way, the default value can be calculated at runtime.

Dart

The example with the optional parameter port can be rewritten as

6. Null-aware operators

There are two null-aware operators which work almost similar in TypeScript and Dart and allow us to write a compact code.

  • Optional chaining operator ?.
  • The logical nullish coalescing operator ??

TypeScript

The optional chaining and nullish coalescing operators are new ECMAScript features. Optional chaining lets us write code where TypeScript can immediately stop running some expressions if we run into a null or undefined. Let's write a code snippet with and without the optional chaining operator.

The nullish coalescing operator returns its right-hand side operand when its left-hand side operand is null or undefined, and otherwise returns its left-hand side operand. For example:

This is a new way to say that the value foo will be used when it's "present"; but when it's null or undefined, calculate bar() in its place. The above code is equivalent to

Dart

In Dart, alle properties and variables are initialized with null per default (every type is an object). Use ?. when you want to call a method or access a property on an nullable object. If the object is null, the result of the method invocation or property accessing is also null. The syntax with the null-aware ?? operator is expr = expr1 ?? expr2. If expr1 is non-null, returns its value; otherwise, evaluates and returns the value of expr2. With null-aware operators you can do nice things like this one:

7. Spread operator

The syntax of the spread operator is three dots (...) followed by an iterable object. It expands the iterable object into individual elements.

TypeScript

The spread operator can be used to expand an array in a places where zero or more elements are expected. Using this, we can e.g. merge two or more arrays or objects. Examples:

Dart

The spread operator in Dart provides a concise way to insert multiple elements into a collection. You can use this operator to insert all elements of a collection into another collection. There is also a null-aware spread operator ...? which helps to avoid exceptions when to be inserted collection is null.

8. Indistinguishability of fields and getters / setters

Getters and setters provide access to the properties of an object. In Java, it’s common to hide all fields behind getters and setters, even if the implementation just forwards to the field. Calling a getter method is different than accessing a field in Java. Getters and setters have different names than the corresponding field names. For example, if you have a field name, the getter would be getName() and the setter setName(String name). Just look at this example in Java:

In TypeScript and Dart, the getters and setters have the same names as the corresponding fields. You can expose a field in a class and later wrap it in a getter and setter without having to touch any code that uses that field. We can say — fields and getters / setters are completely indistinguishable. Let’s look at code snippets in these languages.

TypeScript

Please note, you don’t have to use the method here, just assign the value directly.

Dart

Effective Dart recommends: DON’T wrap a field in a getter and setter unnecessarily.

9. Mixins

Mixin is the process of combining multiple classes to a single target class. It is intended to overcome the limitations of single inheritance model. In Java, TypeScript an Dart, we can’t inherit or extend from more than one class with extends but mixins in TypeScript and Dart helps us to get around that. Mixins create partial classes which we can combine to form a single class that contains all the methods and properties from the partial classes. From a mathematical point of view, one can say that the classic, single super-class inheritance creates a tree (left picture). And mixin pattern creates a directed acyclic graph (right picture).

Composing partial behaviors with mixins in both languages is different.

TypeScript

Let’s create a Timestamped mixin that tracks the creation date of an object in a timestamp property:

Let’s create a User class now.

Finally, we create a new class by mixing Timestamped into User.

Dart

In Dart, we can use the with keyword on every class we want to extend with additional properties and behaviors. Assume, we have a class Person.

Let’s define a class Learner having one method learn() and a class Student which will extend Person and include everything from theLearner.

An instance of type Student can now access both methods info() (from Person) and learn() (from Learner).

10. Dynamic extensions of existing types

Dynamic extensions are a way to add additional functionality to existing libraries without touching them. That means, if you have e.g. a class in a third-party library, you can extend it without changing the class or creating a subclass. This is a killer feature in my opinion which makes a language attractive. There is no something similar in Java at the time of writing.

TypeScript

TypeScript allows merging between multiple types such as interface with interface, enum with enum, namespace with namespace. This feature is called declaration merging. Declaration merging is when the TypeScript complier merges two or more types into one declaration provided the same name. Example:

Why use declaration merging and where does it shine?

  • You can extend declarations of third-party libraries that you import into your project.
  • You can extend declarations of generated TypeScipt definitions, which are usually coming from backend. In my project, we generate TypeScript code from plain Java objects by a Maven plugin.
  • TypeScript uses merging to get different types for the different versions of JavaScript’s standrad libraries. Example: the Array interface. It is defined in lib.es5.d.ts file. By default this is all you get. But if you add ES2015 to the lib entry of your tsconfig.json, TypeScript will also include lib.es2015.d.ts. This includes another Array interface with additional methods like find that were added in ES2015. They get added to the other Array interface via merging.

One project where I was involved and used this technique: OffscreenCanvas by “Definitely Typed” — the repository for high quality TypeScript type definitions.

Dart

In Dart there is also a way to add functionality to existing libraries. This feature is called extension methods. The syntax is extension [<Name>] on <Type> {<Methods>}. The name after the keyword extension is optional. Let’s see some examples how to extend the type int.

Now, we can directly call toBinaryString() on every integer. There are two implementations of the method str() — in this case, we should wrap an integer by the extension name to enables the unambiguous identification of the extension.

Let’s see another example how to extend a generic List.

11. Asynchronous programming with async / await

A long time ago, an asynchronous behavior was modeled using callbacks. Having many nested callbacks led to so called callback hell or pyramid of doom. An example (pseudo code):

Problems with callbacks:

  • Execution order is the opposite of the code order.
  • The code is hard to read.
  • Difficult to run requests in parallel.

Later, many programming languages introduced much better concepts to deal with asynchronism. JavaScript introduced the concept of a Promise to break the pyramid of doom. A Promise represent something that will be available in the future. Dart has the concept of Future. A Future represents the result of an asynchronous operation. It is waiting for the function’s asynchronous operation to finish or to throw an error. If the asynchronous operation succeeds, the Future completes with a value.

What is about Java? Since Java 8 we have CompletableFuture. An example:

The get method waits for the computation to complete, and then retrieves its result. Can we write this kind of code better? Yes, in TypeScript and Dart we can write it better with async and await.

TypeScript

ES2017 introduced the async and await keywords which allow to write the code in a synchronous manner. Anawait is used to wait for a promise to resolve or reject. It can only be used inside an async function. Let’s rewrite the psedo-code above.

The await keywords pause execution of the fetchPages function until each Promise returned by fetch resolves.

Dart

Thanks to Dart’s feature async / await, you might never need to use the Future API directly. So, instead of

you can write the code more straightforward

If you follow Dart’s best practice, the return value should be Future<void>.

12. Generators

Generators (more precisely generator functions) is a handy construct when you need to lazily produce a sequence of values. Both TypeScript and Dart have a built-in support of synchronous and asynchronous generators. With generators, we can create iterable objects where values come synchronously or asynchronously. A generator function is declared with an asterisk (*). A produced sequence of values is emitted with the yield keyword. In this blog post, I will only show how to use asynchronous generators.

TypeScript

If we execute a generator function, an object implementing iterable protocol is returned. The iterable protocol allows an object to be iterable. To iterate over such objects, we should use for … of or for await … of loop respectively. An asynchronous generator is an async function with asterisk. The function body contains one or more await operators. The await operator is used to wait for a promise to resolve or reject.

Dart

Dart has a similar concept as TypeScript. To implement an asynchronous generator function, mark the function body as async*, and use yield statements to deliver values. The return value is a Stream object.

13. Control flow analysis

Both TypeScript and Dart have an excellent support for control flow analysis. The type checker analyses all possible flows of control in statements and expressions to produce the most specific type possible at any given location for a local variable or parameter. Let’s create three classes Pet, Dog and Cat in order to demonstrate the control flow analysis.

TypeScript

In TypeScript, you can create user defined type guards. User defined type guard is just a function that returns a type predicate in the form of someArgument is someType. If the function returns true, TypeScript will narrow the type, so that no cast is required.

As you can see, in the if-statement, we can call bark() without the cast to the Dog. The type of the foo was narrowed automatically by the TypeScript compiler.

Dart

To achieve the same result in Dart, we can use the type test operator is. It returns true if the object has the specified type.

The Dart compiler narrows the type of the foo without the typecast as (as operator is used to cast an object to a particular type).

14. Method chaining

Method chaining allows you to apply a sequence of operations on the same object. Method chaining is often being used for building objects for classes having a lot of properties. A standard implementation of this approach in OOP languages is a builder pattern. Let’s implement a simple example in Java.

By returning this, we can receive the class instance back immediately to call another method in the chain. There is a lot of code, even for this simple implementation without an inner public staticBuilder class (a popular implementation of the builder pattern). TypeScript and Dart can save your time and offer more elegant constructs here.

TypeScript

I like an implementation of the builder pattern with ES6 proxy. The Proxy object enables you to create a proxy for another object, which can intercept and redefine operations for that object. See an explanation for more details. A generic implementation for every use case could be as follows:

The usage is simple. For example:

For more advanced generic implementation look this project on GitHub.

Dart

Dart has a cascaded method invocation. Instead of . notation, the cascade notation uses .. (double-dot) in order to access current modifying instance. Assume, we have a class User with setters / getters for most important fields. We can instantiate a User instance as follows with the cascaded method invocation:

By using cascade, we don’t have to put many of repeated return this inside the class. But we can still return something else appropriate to the methods. Cascades can be nested as well. That allows even faster object buildings. See the example with nested cascades from the Dart language tour. This feature saves you the steps of creating temporary variables and allows you to write more fluid code.

That’s all. Stay tuned!

--

--

Oleg Varaksin
Oleg Varaksin

Written by Oleg Varaksin

Thoughts on software development. Author of “PrimeFaces Cookbook” and “Angular UI Development with PrimeNG”. My old blog: http://ovaraksin.blogspot.de

Responses (1)