TypeScript: JavaScript + Types = Awesome Developer Productivity - Cool TypeScript Features

| Comments

The Mastering the Arcane Art of JavaScript-mancy series are my humble attempt at bringing my love for JavaScript to all other C# developers that haven’t yet discovered how awesome this language and its whole ecosystem are. These articles are excerpts of the super duper awesome JavaScript-Mancy book a compendium of all things JavaScript for C# developers.

In this part of the series we setup a basic TypeScript project and learn some of the cool features in TypeScript that can improve your life as a developer and enhance the code that you produce. Go forth!

Setting up a Simple TypeScript project

The best way to get an idea of the full-blown TypeScript development experience is to setup a simple TypeScript project from scratch and follow along for the rest of the article. As usual, you can download the source code for these and all examples from GitHub.

The easiest way to get started is to install node and npm in your development computer. Once you’ve done that, we’ll install the TypeScript compiler using npm:

1
$ npm install -g typescript

You can verify that the installation has worked correctly by running:

1
2
$ tsc -v
Version 2.4.2

And accessing the TypeScript compiler help:

1
2
3
4
5
6
7
$ tsc -h
Version 2.4.2
Syntax:   tsc [options] [file ...]

Examples: tsc hello.ts
          tsc --outFile file.js file.ts
          tsc @args.txt

I will use Visual Studio Code during these examples but you’re welcome to use any editor that you prefer. Typing this will create a new TypeScript file called hello-wizard.ts and will open it on Visual Studio Code:

1
$ code hello-wizard.ts

Let’s write the canonical hello wizard in TypeScript with a sayHello function:

1
2
3
function sayHello(who: string) : void {
  console.log(`Hello ${who}! I salute you JavaScript-mancer!`);
}

Notice how we have added a type annotation string to the who parameter of this function. If we try to call the function with an argument that doesn’t match the expected type of string the compiler will alert us with a compiler error inside our editor:

1
2
3
sayHello(42);
// => [ts] Argument of type '42' is not assignable 
//         to parameter of type 'string'.

Let’s fix it by saluting yourself. Update the code above to include your name inside a string:

1
sayHello('<Your name here>');

Now you can compile the TypeScript file using the compiler within the terminal (Visual Studio comes with an embedded terminal that you can run inside the editor which is very convenient). Type:

1
$ tsc hello-world.ts

This will tell the TypeScript compiler to transpile your TypeScript application into JavaScript that can run in the browser. It will result in a vanilla JavaScript file hello-world.js that contains the following code:

1
2
3
4
function sayHello(who) {
  console.log("Hello " + who + "! I salute you JavaScript-mancer!");
}
sayHello('<Your name here>');

Beautiful vanilla JavaScript as if you had typed it with your bare hands. You can use node to run this file:

1
2
$ node hello-world.js
Hello <Your name here>! I salute you JavaScript-mancer!

And TaDa! You’ve written, transpiled and run your first TypeScript program! World here we come!

Since it can be slightly tedious to run the TypeScript compiler every time you make changes in your *.ts files, you can setup the compiler in watch mode. This will tell the TypeScript compiler to monitor your source code files and transpile them whenever it detects changes. To setup the TypeScript compiler in watch mode just type the following:

1
2
$ tsc -w hello-world.ts
10:55:11 AM - Compilation complete. Watching for file changes.

In the upcoming sections we will discover some of the great features you can use within TypeScript, all you need to know about TypeScript Type Annotations and what you need to think about when using TypeScript in real-world projects.

Cool TypeScript Features

In addition to type annotations, TypeScript improves JavaScript on its own right with ESnext features and some features of its own.

TypeScript Classes

TypeScript classes come with several features that provide a much better developer experience than ES2015 classes. The first one is class members.

Instead of writing your classes like this:

1
2
3
4
5
6
7
8
9
10
// ES2015 class
class Gladiator {
    constructor(name, hitPoints){
        this.name = name;
        this.hitPoints = hitPoints;
    }
    toString(){
        return `${this.name} the gladiator`
    }
}

You can extract the class members name and hitPoints to the body of the class much like in statically typed languages:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Gladiator {
    name: string;
    hitPoints: number;

    constructor(name: string, hitPoints: number){
        this.name = name;
        this.hitPoints = hitPoints;
    }

    toString(){
        return `${this.name} the gladiator`
    }
}

This can be slightly verbose so TypeScript comes with another feature called parameter properties that allows you to specify a class member and initialize it via the constructor all in one go.

An equivalent version to the one above using parameter properties would look like this:

1
2
3
4
5
6
7
8
class SleekGladiator {
    constructor(public name: string,
                public hitPoints: number){}

    toString(){
        return `${this.name} the gladiator`
    }
}

Better, isn’t it? The public keyword within the class constructor tells TypeScript that name and hitPoints are class members that can be initialized via the constructor.

Moreover, the public keyword gives us a hint as to the last improvement that TypeScript brings to classes: access modifiers. TypeScript comes with four access modifiers that determine how you can access a class member:

  • readonly: Makes a member read only. You must initialize it upon declaration or within a constructor and it can’t be changed after that.
  • private: Makes a member private. It can only be accessed from within the class itself.
  • protected: Makes a member protected. It can only be accessed from within teh class or derived types.
  • public: Makes a member public. It can be accessed by anyone. Following JavaScript ES2015 class implementation, public is the default access modifier for class members and methods if none is provided.

The readonly modifier saves us the necessity to define a @readonly decorator like we did in previous articles.

One shouldn’t be able to change one’s name once it’s been given so let’s make the Gladiator name read-only:

1
2
3
4
5
6
7
class FixedGladiator {
    constructor(readonly name: string,
                public hitPoints: number){}
    toString(){
        return `${this.name}, the gladiator`
    }
}

Now when we create a new gladiator and we give him or her a name it’ll be written in stone:

1
2
3
4
5
const maximo = new FixedGladiator('Maximo', 5000);

maximo.name = "Aurelia";
// => [ts] Cannot assign to 'name' because it is 
//         a constant or a read-only property.

An important thing to note here is that these access modifiers are only applicable in the world of TypeScript. That is, the TypeScript compiler will enforce them when you are writing TypeScript but they’ll be removed when your code is transpiled to JavaScript.

The transpiled version of the FixedGladiator above results in the following JavaScript:

1
2
3
4
5
6
7
8
9
10
var FixedGladiator = (function () {
    function FixedGladiator(name, hitPoints) {
        this.name = name;
        this.hitPoints = hitPoints;
    }
    FixedGladiator.prototype.toString = function () {
        return this.name + ", the gladiator";
    };
    return FixedGladiator;
}());

As you can appreciate from the example above there’s no mechanism which ensures that the name property is read-only.

Next let’s test the private access modifiers. In previous articles we discussed different approaches that you can follow to achieve privacy in JavaScript: closures and symbols. With TypeScript you can achieve data hiding by using the private (and protected) access modifiers.

This was the example we used in White Tower Summoning Enhanced: The Marvels of ES6 Classes to showcase data hiding using closures:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class PrivateBarbarian {

    constructor(name){
        // private members
        let weapons = [];
        // public members
        this.name = name;
        this["character class"] = "barbarian";
        this.hp = 200;

        this.equipsWeapon = function (weapon){
            weapon.equipped = true;
            // the equipsWeapon method encloses the weapons variable
            weapons.push(weapon);
            console.log(`${this.name} grabs a ${weapon.name} ` +
                        `from the cavern floor`);
        };
        this.toString = function(){
          if (weapons.length > 0) {
            return `${this.name} wields a ` +
                   `${weapons.find(w => w.equipped).name}`;
          } else return this.name
        };
    }

    talks(){
        console.log("I am " + this.name + " !!!");
    }

    saysHi(){
        console.log("Hi! I am " + this.name);
    }
};

In it we use closures to enclose the weapons variable which becomes private for all effects and purposes. As you can appreciate from the example, the use of closures forces us to move the methods equipsWeapon and toString that make use of this variable from the body of the class to the body of the constructor function.

The equivalent of this class in TypeScript looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class PrivateBarbarian {
    // private members
    private weapons = [];

    // public members
    ["character class"] = "barbarian";
    hp = 200;

    constructor(public name: string) {}

    equipsWeapon(weapon) {
        weapon.equipped = true;
        // the equipsWeapon method encloses the weapons variable
        this.weapons.push(weapon);
        console.log(`${this.name} grabs a ${weapon.name} ` +
                    `from the cavern floor`);
    }
    toString() {
        if (this.weapons.length > 0) {
        return `${this.name} wields a ` +
                `${this.weapons.find(w => w.equipped).name}`;
        } else return this.name
    };

    talks(){
        console.log("I am " + this.name + " !!!");
    }

    saysHi(){
        console.log("Hi! I am " + this.name);
    }
};

If you now instantiate an indomitable barbarian and try to access the weapons property you’ll be greeted by the following error:

1
2
3
4
const conan = new PrivateBarbarian("shy Conan");
// const privateWeapons = conan.weapons;
// => [ts] Property 'weapons' is private and 
//         only accessible within class 'PrivateBarbarian'.

If you look back and compare both approaches I think that you’ll agree with me that the TypeScript syntax reads better than the ES2015 counterpart. On the flip side, the TypeScript private access modifier is a TypeScript feature that disappears when the code is transpiled to JavaScript, that is, a library consumer that had access to the output JavaScript would be able to access the weapons property of this class. This won’t normally be a problem since most likely your whole development team will be working with TypeScript but there can be some cases where it could be problematic. For instance, for library creators that create their library using TypeScript and make it accessible to consumers that are using vanilla JavaScript.

Why Do I Get An TypeScript Error When Writing An ES2015 class? Isn’t It Valid JavaScript?

Excellent question! When you type the code example with the ES2015 Barbarian class in your TypeScript editor of choice you’ll be surprised to find that the this.name, this.hp and this.equipsWeapon declarations result in a TypeScript compiler error. This is valid ES2015 code and therefore should be valid TypeScript since, as we’ve said before, TypeScript is a superset of ES2015 and any JavaScript is valid TypeScript. So what’s happening?

The reasons for these errors is that TypeScript has different levels of correctness:

  • In the first level the TypeScript compiler examines whether the code is syntactically correct before applying type annotations. If it is, then it is capable of performing transpilation and emitting correct JavaScript code (which is this particular case).
  • In the second level the TypeScript compiler takes a look at the type annotations. According to TypeScript’s type system, the PrivateBarbarian doesn’t have any property name (properties are declared within the body of a class) and therefore it shows the error [ts] Property ‘name’ does not exist on type ‘PrivateBarbarian’.
  • In the third level enabled via the compiler flag --noImplicitAny the TypeScript compiler will become very strict and won’t assume that the type of a non annotated variable is any. That is, it will require that all variables, properties and methods be typed.

Enums

Another great feature in TypeScript are enums. Enums are a common data type in statically typed languages like C# and Java that are used to represent a finite number of things in an strongly typed fashion.

Imagine that you want to express all the different Schools of Elemental Magic: Fire, Water, Air and Earth. When you create diverse elemental spells, these will belong to some of several of these schools and will have advantages and disadvantages against spells of other schools. For instance, a fireball spell could look like this:

1
2
3
4
5
6
7
8
9
10
const fireballSpell = {
  type: 'fire',
  damage: 30,
  cast(target){
    const actualDamage = target.inflictDamage(this.damage,
                                              this.type);
    console.log(`A huge fireball springs from your ` +
        `fingers and impacts ${target} (-${actualDamage}hp)`);
  }
};

The target.inflictDamage would calculate the actualDamage inflicted on a target by taking into account the target resistance to a specific elemental magic or whether it has protective spells against it.

The problem with this example is that strings aren’t very intentional nor provide a lot of information about the Schools of Elemental Magic that are available. In the example above it’d be very easy to have a typo and misspell the string 'fire' for something else.

An improvement over the previous approach is to use an object to encapsulate all available options:

1
2
3
4
5
6
const schoolsOfElementalMagic = {
  fire: 'fire',
  water: 'water',
  air: 'air',
  earth: 'earth'
};

And now we can rewrite our previous example:

1
2
3
4
5
6
7
8
9
10
const fireballSpell = {
  type: schoolsOfElementalMagic.fire,
  damage: 30,
  cast(target){
    const actualDamage = target.inflictDamage(this.damage,
                                              this.type);
    console.log(`A huge fireball springs from your ` +
        `fingers and impacts ${target} (-${actualDamage}hp)`);
  }
};

Awesome! That’s much better than the magic string we had earlier. But it’s still susceptible to typos and there’s nothing stopping you for writing type: 'banana' inside your spell.

That’s were TypeScript enums come in. They give you an statically and strongly typed way to represent a limited collection of things or states. A SchoolsOfMagic enum could look like this:

1
2
3
4
5
6
enum SchoolsOfMagic {
  Fire,
  Water,
  Air,
  Earth
}

This enum allows us to specify an interface that represents the shape of a Spell. Note how a valid Spell has a type property whose type is the enumeration we just created SchoolsOfMagic:

1
2
3
4
5
6
// now we can define a Spell interface
interface Spell {
  type: SchoolsOfMagic,
  damage: number,
  cast(target: any);
}

When we now define a new spell TypeScript will enforce that the type provided for the spell is of type SchoolsOfMagic, and not only that, when using an editor such as Visual Studio Code it will give us all the available options (Fire, Water, Air and Earth) via statement completion.

1
2
3
4
5
6
7
8
9
10
const enumifiedFireballSpell: Spell = {
  type: SchoolsOfMagic.Fire,
  damage: 30,
  cast(target){
    const actualDamage = target.inflictDamage(this.damage,
                                              this.type);
    console.log(`A huge fireball springs from your ` +
        `fingers and impacts ${target} (-${actualDamage}hp)`);
  }
}

If we were to type anything else than the SchoolOfMagic enum (for instance, a string) TypeScript would warn us instantly with the following error message:

1
2
3
4
5
6
// providing other than a SchoolsOfMagic enum would result in error:
// [ts] 
//   Type '{ type: string; damage: number; cast(target: any): void; }' 
//   is not assignable to type 'Spell'.
//   Types of property 'type' are incompatible.
//   Type 'string' is not assignable to type 'SchoolsOfMagic'.

When transpiled to JavaScript enums result in the following code:

1
2
3
4
5
6
7
var SchoolsOfMagic;
(function (SchoolsOfMagic) {
    SchoolsOfMagic[SchoolsOfMagic["Fire"] = 0] = "Fire";
    SchoolsOfMagic[SchoolsOfMagic["Water"] = 1] = "Water";
    SchoolsOfMagic[SchoolsOfMagic["Air"] = 2] = "Air";
    SchoolsOfMagic[SchoolsOfMagic["Earth"] = 3] = "Earth";
})(SchoolsOfMagic || (SchoolsOfMagic = {}));

At first sight it may look a little bit daunting. But let’s decompose it into smaller statements:

1
2
3
4
5
6
7
8
// Set 'Fire' property in SchoolsOfMagic to 0
SchoolsOfMagic["Fire"] = 0;

// it evaluates to 0 so that this:
SchoolsOfMagic[SchoolsOfMagic["Fire"] = 0] = "Fire";
// is equivalent to:
SchoolsOfMagic[0] = "Fire";
// which means set '0' property in SchoolsOfMagic to 0

So an enum represents a two-way mapping between numbers and strings with the enum name. Just like you can specify the names, you can select the numbers when declaring the enum:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Start in 1 and increase numbers
enum SchoolsOfMagic {
  Fire=1,
  Water,
  Air,
  Earth
}

// Explicitly set all numbers
enum SchoolsOfMagic {
  Fire=2,
  Water=4,
  Air=6,
  Earth=8
}

// Computed enums
enum SchoolsOfMagic {
  Fire=1,
  Water=Fire*2,
  Air=2,
  Earth=Air*2
}

Whenever we don’t want for the transpiled JavaScript to contain reference to enums (for instance, in a constrained environment were we want to ship less code) we can use const enums. The following enum definition will not be transpiled to JavaScript:

1
2
3
4
5
6
const enum SchoolOfMagic {
  Fire,
  Water,
  Air,
  Earth
}

Instead it will be inlined and any reference to Fire, Water, Air and Earth will be replaced by a number. In this case 0, 1, 2, 3 respectively.

Still prefer strings? Check This String Literal Types

If you still prefer vanilla strings TypeScript has the ability to create types based of a series of specific valid strings. An equivalent for our schools of magic could look like this:

1
type SchoolsOfMagic = "fire" | "earth" | "air" | "water";

Again we define an interface in terms of this new type:

1
2
3
4
5
interface Spell {
  type: SchoolsOfMagic,
  damage: number,
  cast(target: any);
}

And we’re ready to create spells. Using anything other than the allowed strings will result in a transpilation error:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const FireballSpell: Spell = {
  type: "necromancy",
  damage: 30,
  cast(target){
    const actualDamage = target.inflictDamage(this.damage, this.type);
    console.log(`A huge fireball springs from your ` +
        `fingers and impacts ${target} (-${actualDamage}hp)`);
  }
}
// => [ts] 
//  Type '{ type: "necromancy"; damage: number; cast(target: any): void; }' 
//  is not assignable to type 'SpellII'.
//  Types of property 'type' are incompatible.
//  Type '"necromancy"' is not assignable to type 'SchoolsOfMagicII'.

Object Spread and Rest

In earlier articles we saw rest paremeters and the spread operator brought by ES2015.

As you can probably remember, rest parameters improve the developer experience of declaring functions with multiple arguments 1. Instead of using the arguments object like we used to do prior to ES2015:

1
2
3
4
5
6
7
8
9
10
11
12
function obliterate(){
  // Unfortunately arguments is not an array :O
  // so we need to convert it ourselves
  var victims = Array.prototype.slice.call(arguments,
                              /* startFromIndex */ 0);

  victims.forEach(function(victim){
    console.log(victim + " wiped off of the face of the earth");
  });
  console.log('*Everything* has been obliterated, ' +
              'oh great master of evil and deceit!');
}

We can use rest syntax to collect all incoming arguments directly into an array victims:

1
2
3
4
5
6
7
function obliterate(...victims){
  victims.forEach(function(victim){
    console.log(`${victim} wiped out of the face of the earth`);
  });
  console.log('*Everything* has been obliterated, ' +
              'oh great master of evil and deceit!');
}

On the other hand the spread operator works sort of in an opposite way to rest parameters. Instead of taking a variable number of arguments and packing them into an array, the spread operator takes and array and expands it into its compounding items.

Following this principle the spread operator has many use cases2. Like concatenating arrays:

1
2
3
let knownFoesLevel1 = ['rat', 'rabbit']
let newFoes = ['globin', 'ghoul'];
let knownFoesLevel2 = [...knownFoesLevel1, ...newFoes];

Or cloning them:

1
2
let foes = ['globin', 'ghoul'];
let clonedFoes = [...foes];

Object Spread and Rest brings this same type of functionality that is available in Arrays to Objects.

A great use case for the Object spread operator are mixins. In previous articles we used Object.assign to mix the properties of two or more different objects. For instance, in this Wizard factory function we mix the wizard properties with mixins that encapsulate behaviors to identify something by name and cast spells:

1
2
3
4
5
6
7
8
9
10
function Wizard(element, mana, name, hp){
  let wizard = {element,
                mana,
                name,
                hp};
  Object.assign(wizard,
               canBeIdentifiedByName,
               canCastSpells);
  return wizard;
}

We can rewrite the example above using object spread as follows:

1
2
3
4
5
6
7
8
9
10
11
12
function Wizard(element, mana, name, hp){
  let wizard = {element,
                mana,
                name,
                hp};

  // now we use object spread
  return {...wizard,
          ...canBeIdentifiedByName,
          ...canCastSpells
         };
}

The object spread operator essentially says: get all properties of wizard, canBeIdentifiedByName and canCastSpells and put them together within the same object. If there are any properties that have the same name, the last one wins and overwrites the first.

The opposite to object spread are object rest parameters. They work in a similar fashion to ES2015 rest parameters and are particularly helpful together with ES2015 destructuring.

If you remember, we used destructuring and rest parameters to extract elements from an array:

1
2
3
4
let [first, second, ...rest] = ['dragon', 'chimera', 'harpy', 'medusa'];
console.log(first); // => dragon
console.log(second); // => chimera
console.log(rest); // => ['harpy', 'medusa']

With the Object Spread Operator we can follow the same pattern to extract and collect properties from objects:

1
2
3
4
5
6
7
8
9
10
11
12
13
let {name, type, ...stats} = {
  name: 'Hammer of the Morning',
  type: 'two-handed war hammer',
  weight: '40 pounds',
  material: 'nephirium',
  state: 'well kept'
};
console.log(name); // => Hammer of Morning
console.log(type); // => two-handed war hammer
console.log(stats);
// => {weight: '40 pounds', 
//     material: 'nephirium', 
//     state: 'well kept'}

And There’s More!

There’s a lot more features in TypeScript that expand on ES2015 either via early implementation of ESnext features that are currently in a proposal stage (like async/await or decorators ) or via entirely new features like the ones we’ve seen related to classes and enums.

If you’re interested into learning more about TypeScript then I encourage you to take a look at the TypeScript handbook and at the release notes both of which provide detailed information about what TypeScript has in store for you.

Up Next

Up next you’ll learn about that secret sauce that makes TypeScript special: Type Annotations.

Get the Book!

If you enjoyed this article take a look at the JavaScript-mancy OOP: Mastering the Arcane Art of Summoning Objects, a compendium of OOP techniques available in JavaScript, ES2015, ESNext and TypeScript.

JavaScript-mancy OOP: Mastering the Arcane Art of Summoning Objects Book Cover

Have an awesome day! :)


  1. Like params in C#.

  2. Go back and review JavaScript-mancy: Getting Started for lots more of use cases!

Comments