14 language features in TypeScript and Dart you may miss in Java
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 anotherArray
interface with additional methods likefind
that were added in ES2015. They get added to the otherArray
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!