In-Depth
A TypeScript Primer
Whether you're new to JavaScript or well-versed in all it has to offer, TypeScript is a compelling option.
TypeScript was developed to capitalize on the strengths of JavaScript while simultaneously shoring up its weaknesses. JavaScript is likely the most widely used programming language in the world, and at the same time, possibly the most disliked. Although almost 20 years old, it has some of the most-advanced language features available even today, while still harboring characteristics that were recognized as flaws long before JavaScript was born.
This article dives into the details of TypeScript and its features, examining everything from the tooling to the specific syntax that it offers. Before I can do that, however, it's necessary to examine the "why" behind TypeScript. In other words, which JavaScript characteristics does TypeScript set out to reuse, and which ones does it try to curb?
JavaScript: The Good
Most important, JavaScript offers ubiquity. It provides a single development platform that executes on virtually all modern computers -- whether from mobile devices, tablets or high-end servers. This single feature alone is the key to the vast success of JavaScript and its selection over other languages that, from a language design perspective, are significantly better.
Although the best JavaScript feature is its ubiquity, that isn't the only thing in its favor. One of the more advanced features of JavaScript is support for function expressions; this has proven to be the key characteristic, enabling it to achieve some semblance of object-oriented programming (OOP). Through function expressions, JavaScript is able to support constructs such as classes, encapsulation and inheritance, to name a few.
Another critical language feature required for OOP is closure, because the combination of closure and function expressions allows data to be grouped with behavior. By definition, closure allows the referencing of variables defined in one scope from within the context of a second scope, such as a reference to a local variable from within a function expression (the equivalent of an anonymous method or lambda expression in the Microsoft .NET Framework). By nesting data and function expressions within a parent function expression, a JavaScript "class" is defined -- one that might even include encapsulation.
Another compelling characteristic is the vibrancy of the development community and the frameworks being created. Open source libraries such as Knockout, jQuery and Modernizr, to name just a few, provide significant enhancements to JavaScript. This community has had a tremendous influence on the establishment of JavaScript as a mainstream language.
JavaScript: The Bad
Along with its good points, few would deny that the huge popularity of JavaScript is not a result of its impressive set of language features.
Object-Oriented via Discipline
To start with, even the basics of object-oriented constructs are lacking. While such constructs certainly exist, they're not intrinsic patterns of JavaScript; instead, they're the result of disciplined structure that developers impose on the language. This is true for basic constructs such as classes, and even more so for encapsulation and inheritance. They weren't intentionally placed through language design, but imposed after the fact.
Much of the object-oriented structure of JavaScript is achieved through the reuse of function expressions and the encapsulation of such expressions within other (containing) function expressions. Consider, for example, providing disambiguating types via the use of a namespace. In JavaScript, this is accomplished by embedding within an outer function expression (representing the namespace) another function representing a class. Then, to declare a member function, a prototype definition is embedded within the function that represents the class. The definition of Person in Listing 1 provides an example.
Listing 1. Person definition.
var TypeScriptingStuff;
(function (TypeScriptingStuff) {
var Person = (function () {
function Person(FirstName, LastName) {
this.FirstName = FirstName;
this.LastName = LastName;
}
Person.prototype.getFullName = function () {
return this.FirstName + " " + this.LastName;
};
return Person;
})();
TypeScriptingStuff.Person = Person;
})(TypeScriptingStuff || (TypeScriptingStuff = {}));
Supporting a namespace-qualified simple class (one that supports two member variables, one member function and a constructor) goes beyond what would typically be considered basic syntax. It isn't unmanageable on its own, but using the same syntax for an enterprise library of types is less than efficient. The core problem is all the "ceremony" that the language requires in order to enable object-oriented features.
Dynamic No Matter What
Arguably, the main criticism of JavaScript comes from strong-typing "bigots" who celebrate the fact that when they mistakenly code against a nonexistent API, they receive a compile warning even before they execute any code.
This doesn't mean that dynamic support shouldn't be available; even strongly typed languages like C# support dynamic typing, and for good reason. But JavaScript dynamic support is the only option, whereas C# support is an opt-in paradigm.
In contrast, JavaScript requires dynamic typing. The result is that simple typos don't appear until runtime (hopefully in unit tests). Simply using the incorrect case, such as "Id" rather than "ID" or "id," will go unnoticed until the code is executed. Problems like this become especially problematic (and common) when using libraries such as Knockout, as they fundamentally change how a type is defined. Consider the bug in the Knockout observable type representing a Task in this code:
var TaskModel = (function () {
function TaskModel(description, completed) {
this.Description = "";
this.Completed = ko.observable(completed);
}
return TaskModel;
})();
The intention of this code would be for all members to be observable, so that changes would automatically be rendered in the UI. Unfortunately, without type declaration and then type checking, there's no way to implicitly flag this code's problem -- that assigning an empty string to Description (rather than a ko.observable type value, as was assigned to Completed) results in a property that's unobservable. More generally, mismatched type assignments are not implicitly discoverable in loosely typed languages like JavaScript without executing the code -- delaying bug discovery far later in the development cycle than is desirable.
Admittedly, strong typing is more verbose, and detractors would argue the additional ceremony is pointless. Its value rests on two premises:
- The closer to code authoring (ideally inline, as it's typed) that a bug is identified, the better.
- Any implicit identification of bugs (without explicit action such as writing unit tests) is more valuable than runtime mechanisms. (By no means is this an excuse to avoid writing unit tests. Rather, strong typing results in more robust and correct code before you even get to executing the unit tests -- eliminating a host of issues that would otherwise have to be diagnosed.)
Idiosyncratic
There are numerous other idiosyncrasies in the JavaScript language. Table 1 lists of some of these quirks.
Global variables allowed, implied (such as when a variable is not declared), and even required |
document.write(
"<"h1>Hello! My name is Inigo Montoya.</h1>"); |
Implicit semicolon insertion, sometimes |
function () {
var func1 = function() {
return {
};
}
var func2 = function() {
return
{ }
};
expect(func1() == func2()).toBe(false);
}
|
== does type coercion before comparison, resulting in seemingly random transitivity |
function () {
expect(0 == '0').toBe(true);
expect(0 == '').toBe(true);
expect('' == '0').toBe(false);
expect(false == 'false').toBe(false);
expect(false == '0').toBe(true);
expect(" \t\r\n " == 0).toBe(true);
expect(null == undefined).toBe(true);
}
|
Mathematics isn't accurate |
function () {
expect(0.1 + 0.2).toBe(0.30000000000000004);
}
|
Table 1. JavaScript idiosyncrasies.
|
By no means is Table 1 comprehensive; it just points out some of the more-common gotchas that inattentive programmers might encounter.
TypeScript to the Rescue
TypeScript was created to preserve the flavor of JavaScript and keep the good parts, while addressing a significant number of the bad parts. It's the combination of a prebuild lint tool type and a language specification. The prebuild lint tool portion executes against an enhanced JavaScript-like syntax -- the TypeScript language -- to enable both strong typing and inherent object-oriented constructs at compile time.
The preservation of JavaScript is maintained in essentially two ways. First, the TypeScript compiler, tsc.exe, compiles a TypeScript file (*.ts by convention) into a JavaScript file (*.js). In other words, the output of a successful TypeScript file is an ECMA3- (default) or ECMA5-compliant JavaScript file. As such, the TypeScript essentially achieves the same ubiquity of JavaScript -- the compiler-produced .js files will be compatible with any and all modern browsers. This is critical, because it means there's no requirement that Internet Explorer (or any other browser, for that matter) be updated to support TypeScript. This isn't a sinister plot on Microsoft's part to lock the world into using its browser; on the contrary, Microsoft has intentionally supported all browser platforms since the inception of TypeScript. Furthermore, the source code for the entire TypeScript implementation is available under an open source Apache License 2.0, allowing developers to update and improve the source code or even fork the code to enhance the tooling.
The second way the TypeScript compiler preserves JavaScript is by allowing existing JavaScript to remain intact, rather than forcing the porting of it over to TypeScript. You can take an existing block of JavaScript code and inject it into a TypeScript file, only to have the JavaScript extracted and embedded into a .js file. In fact, in order to stay true to JavaScript, the TypeScript developers targeted compatibility with ECMAScript 6 in terms of classes, modules and arrow functions. These factors demonstrate that the goal of TypeScript is to provide a better JavaScript, while still strongly preserving the feel of JavaScript.
As mentioned, the two key features TypeScript brings to JavaScript are strong typing and inherent object-oriented constructs. These are important because wherever possible, the compiler should be verifying intent rather than waiting until a developer writes some unit test to discover an issue; or, even worse, the bug gets distributed and discovered by an end-user instead.
TypeScript Tooling
There are essentially two main distribution mechanisms for TypeScript tooling:
- As a Node.js package.
- As a Visual Studio 2012 plug-in, leveraging the command-line compiler tsc.exe.
As a plug-in, it's fully integrated into the Visual Studio IDE, offering IntelliSense, a project and file wizard, continuous compilation/type checking, debugging and even refactoring support for rename (see Figure 1).
The TypeScript compiler integrates smoothly into the build process by running as a build step immediately prior to the execution of additional compilation steps such as the execution of a minifier or even an additional lint compatibility checker. Essentially, the TypeScript compiler takes the *.ts input file and outputs *.js files against which you can continue to execute any other additional tooling you may want.
Object-Oriented Constructs
For an overview of the object-oriented characteristics of TypeScript, consider Listing 2.
Listing 2. Declaring a Class in TypeScript.
// Module (namespace equivalent)
module TypeScriptingStuff {
// Class
export class Person {
// Constructor
constructor(public FirstName: string, public LastName: string) {
};
// Instance function member
public getFullName(): string {
return this.FirstName + " " + this.LastName;
};
}
}
This code is the exact TypeScript equivalent of the JavaScript shown in Listing 1. One method for evaluating TypeScript is to simply ask the question of whether it would be easier to write, understand, and maintain the TypeScript code or the JavaScript equivalent.
The appeal of TypeScript (especially for .NET developers) is the native TypeScript support for object-oriented concepts: modules (or namespaces), classes, constructors, properties (the equivalent of a C# field), and access modifiers such as public and private. Each of these is relatively intuitive from the listing -- except, perhaps, support for properties. Property support is less intuitive: for example, Listing 2 uses shorthand for the properties set by the constructor. Rather than explicitly assigning this.FirstName and this.LastName (as in JavaScript no field declaration is necessary, just assignment), the implementation is implied via the parameter modifier public. The equivalent explicit implementation is shown in Listing 3.
Listing 3. Explicit constructor-to-field assignment.
export class Person {
FirstName: string;
LastName: string;
// Constructor
constructor(firstName: string, lastName: string) {
this.FirstName = firstName;
this.LastName = lastName;
};
// ...
};
Another important object-oriented principle is polymorphism, via either inheritance or interface implementation. Here, again, TypeScript provides first-class support, as shown in Listing 4.
Listing 4. Interfaces, inheritance and polymorphism.
// Interface
export interface IEntity {
getID(): number;
}
// Inheritance and Interface Implementation
export class Employee extends Person implements IEntity {
constructor(
public FirstName: string,
public LastName: string,
public EmployeeID: number) {
super(FirstName, LastName);
Employee.instanceCount++;
};
public static instanceCount: number;
getID(): number {
return this.EmployeeID;
}
public getFullName(): string {
return this.FirstName + " "
+ this.LastName + " (" + this.EmployeeID + ")";
};
}
// Polymorphism
class Program {
static Main() {
var employee: Employee;
var person: Person;
employee = new Employee("Inigo", "Montoya", 42);
AssertEquals(employee.getID(), 42);
person = employee;
AssertEquals(person.getFullName(), "Inigo Montoya (42)",
"The person.getFullName() was not what was expected.");
}
static AssertEquals(expected: any, actual: any, message?: string):void {
if (expected !== actual) {
throw "Expected: " + expected + "\nActual: " + actual +
"\n" + message;
}
}
}
Notice in Listing 4 the declaration of an interface, then the implementation of that interface within the Employee class. In addition to implementing IEntity, the Employee class also extends (inherits from) the Person class. Leveraging this inheritance relationship, it's possible to assign an Employee object to a Person variable. Then, when calling getFullName on the Person variable, the Employee's implementation gets invoked, leveraging the polymorphic capabilities of TypeScript (via the underlying JavaScript support, as always).
There are a number of other OOP constructs to point out in Listing 4:
- Static. Notice that both methods and fields can be explicitly declared as static, and doing so sets their scope to be type-based rather than instance-based -- that is, qualified with the type name rather than this.
- Public by default. There's no access modifier on getID, but it's still callable from Program.Main because members are public by default. This is in keeping with the JavaScript characteristic in which data isn't encapsulated by default.
- Functions are allowed to have optional parameters, as shown in the Assert method.
- Super. Derived classes can invoke members in their base class using the super keyword.
In addition (although not shown), TypeScript supports method overloading.
Strong Typing
I've looked at the object-oriented features related to TypeScript, but object-oriented structures are far less valuable without strong typing. Strong typing is the second core aspect of TypeScript, and it (optionally) permeates virtually every aspect of the language.
In TypeScript, the compiler executes and validates type compatibility. Once a variable's declared as a specific type, any access of that variable will result in compile-time (not runtime) validation that the access conforms to the API associated with that type.
For example, if a variable is declared as a number, the compiler will report an error if a string is assigned to the variable, as would the attempted invocation of the nonexistent toLowerCase function. The concept of type is so strong, in fact, that TypeScript doesn't allow accessing a variable that hasn't been declared. Yes, variables must be declared before accessing -- a declaration will explicitly identify the type or implicitly assume it's the Any type. The result is a language more verbose than straight JavaScript, but also one that potentially eliminates typo errors such as the Id example from earlier.
Through TypeScript strong typing, another JavaScript idiosyncrasy is eliminated: seemingly random transitivity. Because TypeScript evaluates the types of objects when accessed, it's able to prevent comparisons between unrelated types. As a result, it's no longer possible to fall into the trap mentioned earlier, when the comparison of unrelated types produced unexpected results. The following comparison, for example, wouldn't be valid in TypeScript:
expect(0 == '0').toBe(true);
It's important to remember that TypeScript is a compile-time language that generates equivalent JavaScript code -- code that doesn't have the type-safety characteristics of TypeScript. In other words, although the TypeScript compiler performs type checking, no such type checking occurs at runtime (unless it was already part of the JavaScript execution behavior). As such, there is literally no runtime impact, performance or otherwise, of the type checking provided by TypeScript.
Data Types
In order to achieve strong type, it's obviously necessary to have data types and be able to specify the types for all variable and even function declarations. As you might have already noticed from earlier listings, variables can (optionally) be declared with a data type. As a result, assignment of an incompatible type will result in a compile-time error indicating that one type cannot successfully convert to another.
The declarable, primitive data types available in TypeScript are String, Number and Boolean, with corresponding keywords string, number and bool. Even though these types are primitive, they can be assigned a value of null (yes, including number). Each of these types corresponds to the equivalently named underlying JavaScript type.
As in JavaScript, there are also two additional TypeScript primitive types:
- Null: The type of the one-and-only literal value null.
- Undefined: The type assigned when a variable isn't declared as a specific type, and no value is assigned.
There are of course a host of additional "custom" types available. These object types are either literal types or else the results of type declarations using the keywords class, module and interface. Much of the basics of an object type declaration can be found in Listings 2 and 4.
If a variable is declared without a type (for instance, var v), the data type will default to the Any type, representing any JavaScript value and referenced using the any keyword. The result is a variable for which no static type checking occurs (like dynamic in C#).
With an Any type, member access occurs without any validation by the TypeScript compiler. The availability of the Any type is what allows type checking within TypeScript to be an opt-in model. In other words, by specifying the data type, you're declaring type checking should occur. In contrast, declaring a variable without a type or explicitly specifying the Any type is the means by which variables within TypeScript forgo type checking.
Function Types
As already stated, the most important feature of JavaScript from a language perspective is probably function expressions. Setting aside all the object-oriented features that function expressions make possible, at their core function expressions are a means of passing behavior as a parameter (the equivalent of a delegate or lambda expression in C#).
For example, rather than writing numerous different routines that sort and print out/display data, with function expressions it's possible to instead implement multiple sort algorithms and pass each one into the same print/display method. Like JavaScript, TypeScript includes support for a function expression with a function type. Because it's a type, it can be used in any location that a type can appear, whether declaring a variable or specifying a callback parameter on a function. The syntax for the function type borrows heavily from the lambda expression syntax of C#, as shown in Listing 5.
Listing 5. Working with function types.
static delegateSample() {
var items: string[] =
['doc', 'bashful', 'dopey', 'sneezy', 'grumpy', 'happy', 'sleepy'];
var comparer = (first: string, second: string): number => {
return first.localeCompare(second);
};
sort(items, comparer);
}
static sort(items: string[],
comparer: { (first: string, second: string): number; }) {
return items.sort(comparer);
}
This listing demonstrates that function types themselves are entirely type safe; the parameters and the return (optionally) have types associated with them. Furthermore, when assigning a function literal to a function variable (such as the comparer parameter of the sort function in this case), TypeScript will verify the type compatibility and report a compile time error if they're not aligned. As with C#, the type safety associated with function types makes it possible to provide full support for IntelliSense: the type (and therefore the available members) is always known. (Notice, in addition, the syntax borrows heavily from C# lambda expressions.)
It's worth noting, in association with both function and function expressions, that there's also the void keyword, which is used to identify when there's no return from a function.
Learn TypeScript -- Love TypeScript
Developers faced with JavaScript development can either stick their heads in the sand or embrace it. Fortunately, for those used to the .NET world -- with its first-class, object-oriented constructs and compile-time API verification -- all is not lost. Building on top of JavaScript, TypeScript provides a means to achieve both OOP and type safety, all while maintaining the ubiquity of JavaScript.
Whether you're new to JavaScript or well-versed in all it has to offer, TypeScript is a compelling option. It's relatively low-cost to embrace, both initially and in the unlikely case you need to abandon it. For one thing, the compatibility between TypeScript and JavaScript is so high that it's possible to code in JavaScript alongside TypeScript. This includes the option to create (or download) *.d.ts files, which are similar in purpose to C++ header files, enabling import of existing JavaScript libraries.
Additionally, at the point you might decide to abandon TypeScript, you can begin using the .js files that the TypeScript compiler generated. In fact, these files are relatively easily to read, using a well-structured style that conforms strongly to best practices. Good style is so characteristic, in fact, that I've continued to execute JSLint (via SharpLinter) against the TypeScript compiler's .js files, and have found it to be extremely clean.
One last point: TypeScript development hasn't ended. In fact, the language specification makes reference to several improvements, including support for enums and even generics.
TypeScript offers significant improvements over JavaScript as a language, while staying true to the qualities that make JavaScript compelling. Furthermore, the cost and commitment to TypeScript is relatively minor, allowing it to be abandoned without much consequence if for some reason it proves to be suboptimal for you. That's why I'd encourage the evaluation and adoption of TypeScript as the language of choice whenever JavaScript development is required.