TypeScript vs. Dart: Class Constructors
TypeScript, built by Microsoft, is a typed superset of JavaScript that compiles to plain JavaScript. Its mission is to add a level of type safety and object-oriented program structure to web apps, making it easier to build at scale.
Dart, built by Google, is a general-purpose application programming language that can also compile to plain (optimized) JavaScript. It, too, supports strong typing and real classes. For web apps, Dart's mission is similar to TypeScript's, but the approach is quite different.
I've had the opportunity to work extensively with both languages, even writing virtually the same code in each, and I think a frank comparison of the two is overdue. It's a big job, so I'll be tackling it in bite-sized chunks, starting with how class constructors work.
This comparison is current as of TypeScript 2.1 and Dart 1.21.1.
You do need to be familiar with the basics of object-oriented programming syntax in order to get the most out of this article.
Basic Constructor Syntax
In both languages, providing a constructor is optional, though if it is omitted in Dart, a default (no-argument) constructor is automatically generated.
Let's take a look at how each language declares a class constructor.
TypeScript
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
In this basic example, you can see that most of the syntax is familiar if you come from a C++, C#, or Java background. Since TypeScript's class syntax is just window dressing over an ES5 function, it requires you to explicitly call out the constructor code using the special name constructor
. This is a departure from most curly-brace OOP languages. Also, types are placed to the right of function parameters, following a colon, which is also atypical. The number
type matches JavaScript's version, which is always a 64-bit floating point.
Within the body, you use this
to differentiate between member variables and function parameters with the same name, though it's important to note that even if there were no name conflict, you must use this
to access member variables with TypeScript.
Compiled to JavaScript, this example would look something like this self-executing anonymous function, using closures to simulate member variables:
var Point = (function () {
function Point(x, y) {
this.x = x;
this.y = y;
}
return Point;
})();
Dart
The Dart version, by contrast, looks quite a bit more like traditional OOP languages. The constructor's name matches the class name, and the types are on the left, sans colon.
class Point {
num x;
num y;
Point(num x, num y) {
this.x = x;
this.y = y;
}
}
Dart uses the more succinct num
type, which is actually ancestor to the int
and double
types. Unlike TypeScript, Dart has support for more specific number types, though num
covers both. When compiling to JavaScript, the specificity is lost, but native Dart code runs server-side in the Dart virtual machine, where the types remain distinct. During development, you can run Dart web apps in a special "checked mode" that does warn of violations of intent, such as when a number with a fractional component is assigned to a variable of type int
(integer).
Like in the TypeScript version, this
is used to disambiguate variable access, but in Dart, the use of this
is optional due to support for lexical scope. If the parameters were named differently, you could do something like this:
class Point {
num x;
num y;
Point(num xValue, num yValue) {
x = xValue;
y = yValue;
}
}
More About Constructor Parameters
Both languages provide an easy way to avoid the situation illustrated in that last code example, where verbose and unnatural parameter names were used purely for disambiguation.
TypeScript
With TypeScript, you can declare and assign member variables right in the constructor parameter list, like so:
class Point {
constructor(public x: number, public y: number) {
}
}
This can save you some typing. The x
and y
parameters are automatically elevated to member variable status and arguments passed in are assigned to them. It could be argued that this comes at the cost of some clarity, since you may have class member variables declared in multiple places, but it works all right in simple cases, or perhaps with careful formatting:
class Point {
id: string;
total: number;
constructor(
public x: number,
public y: number) {
}
}
Note that these parameter properties can be qualified with any valid accessibility modifier, such as public
, private
, protected
, or readonly
, just as the more traditional declarations can.
Dart
Dart also provides some syntactic sugar to tackle the common scenario of assigning a constructor argument to an instance variable.
class Point {
num x;
num y;
Point(this.x, this.y);
}
Constructor bodies in Dart are optional, whereas with TypeScript, you still need the opening and closing braces even if the body does nothing. Dart won't let you declare new member variables in your parameter list, but the this
syntax does automatically assign argument values to declared variables of the same name. If there were a constructor body, it would run after these assignments completed.
Initializer Lists
Dart also supports initializer lists for initializing variables before executing the constructor's body. This can be valuable when something more complex than simple assignment needs to occur, or when you need to initialize final
variables.
class Point {
final num x;
final num y;
Point(Map jsonObj) :
x = jsonObj['x'],
y = jsonObj['y'];
}
A Dart Map is akin to JavaScript's basic object type, like an associative array or hashmap. Here, x
and y
are assigned values from within that object before any constructor body is executed, which, again, is important because member variables marked as final
must be initialized early.
TypeScript has no support for initializer lists. TypeScript class properties marked readonly
must be initialized at their declaration or in the constructor body.
Overloading Constructors
Java, C++, and C# support true constructor overloading, but TypeScript and Dart take different approaches to this feature. It's basically when your class has multiple constructors with which you can create a new instance, typically differentiated by the types and/or number of parameters.
The Problem
You want to be able to create a Point instance in several different ways, either by directly providing number values or passing an object containing those values.
With a deep knowledge of either language, it's possible to solve this problem in any number of ways, some readable and sane, and some not so much. We don't have time/energy to discuss them all here, so this comparison will explore a typical approach for each language, but not necessarily the only one.
TypeScript
In trying to be a friend to JavaScript, TypeScript takes on the burden of some unfortunate limitations. Since JavaScript does not support function overloading of any kind, TypeScript's constructor overloading ends up being a bit strange, in my view.
Essentially, you provide the overloaded signatures of the constructor you'd like to support, followed by one "real" constructor, which will actually be called at run-time. Here's an example:
interface IPointData {
x: number;
y: number;
}
class Point implements IPointData {
x: number;
y: number;
constructor(x: number, y: number);
constructor(data: IPointData);
constructor(xOrData: any, y?: number) {
if (typeof xOrData === "number") {
this.x = xOrData;
this.y = y;
} else {
this.x = xOrData.x;
this.y = xOrData.y;
}
}
}
p1: Point = new Point(5, 5);
p2: Point = new Point({x: 5, y: 5});
p3: Point = new Point(5); // ERROR: Not a valid overload.
The first constructor takes two numbers; pretty typical there. The second one takes an object, and you can make use of TypeScript's interface feature to make sure the passed object has the expected structure. The third constructor is the "real" one, the one that is actually called and executed no matter which of the overloaded constructors is used. In fact, you can only define a body for one constructor in a TypeScript class, and it needs to handle all the variations, which is why the y
parameter is marked as optional (y?
) in the last one.
The p1
and p2
variables demonstrate the use of the overloaded constructors, while p3
is invalid because there is no version of the constructor that takes a single number. Remember, you can't actually use the third constructor to create an instance, but it is the one that executes every time.
For a deeper exploration into other approaches you can take to this problem in TypeScript, take a look at this StackOverflow question.
Dart
The Dart language doesn't support function overloading. Instead, a relatively novel feature called "named constructors" is used.
class Point {
num x;
num y;
Point(this.x, this.y);
Point.fromJson(Map<String, num> jsonObj) {
x = jsonObj['x'];
y = jsonObj['y'];
}
}
Point p1 = new Point(5, 5);
Point p2 = new Point.fromJson({"x": 5, "y": 5});
Point p3 = new Point(5); // ERROR: Not a valid constructor.
Dart's approach takes quite a bit less code, mostly because named constructors are distinct functions, instead of funnels into one true constructor. Also, you don't need to use this
everywhere, and I think you'd be surprised at how much less noisy the code is without it. The named constructor could make its assignments in an initializer list, omitting the body, if necessary.
Every class in Dart exports an interface (as opposed to TypeScript's explicit interfaces), but using an interface to constrain the value passed to the Point.fromJson
constructor wouldn't be typical. Take a look at the following declarations:
Point p4 = new Point.fromJson({"i": 5, "z": 5});
Point p5 = new Point.fromJson({"i": "Hi.", "z": 5});
The TypeScript equivalent of the p4
declaration would flag an error because the argument does not adhere to the IPointData interface, while Dart lets it slide. It's possible to use interfaces similarly in Dart, but not quite as straightforward, as you can't prescribe a strict structure for a Map. Generics are used to constrain the types of the keys and values (Map<String, num>
), so the p5
assignment would create an error, since there is a string value in there. If it were crucial that this constructor be as strict as the TypeScript version, you'd create a PointData class and pass an instance of that instead of a Map.
But wait, there's more!
Dart also has constant constructors, factory constructors, and more. Turns out comparing languages, even when you focus on a single feature, is kind of a lot of work! Perhaps a future article will continue the discussion.