DEV Community

Cover image for TypeScript: JavaScript + Types = Awesome Developer Productivity
Jaime González García
Jaime González García

Posted on • Originally published at barbarianmeetscoding.com

TypeScript: JavaScript + Types = Awesome Developer Productivity

This article is a chapter from JavaScript-mancy OOP: Mastering the Arcane Art of Summoning Objects in JavaScript a breathtaking story of EPIC proportions, awkward sense of humor and saving the world one JavaScript function at a time.

JavaScript-mancy is a dangerous art.
Get an incantation slightly wrong,
and anything can happen. 

More than one young apprentice
has found out about this the hard way,
just a split second before 
razing a village to the ground,
letting loose a bloodthirsty beast,
or making something unexpected explode.

That's generally the way it is.

There are records of an ancient order,
an order of disciplined warrior monks,
who were able to tame the wild winds of magic.
But too little of them remains.

Did they exist? 
Or are they just wishful thinking and myth?

        - The Chronicler
Enter fullscreen mode Exit fullscreen mode
/* 
The group walks into the chamber and 
the door closes itself behind them...

...the chamber is poorly lit. A metal brazier of eerie blue 
flames lies in the middle of the room and bathes a strange 
obsidian obelisk in a mysterious light. Huge columns 
surround the obelisk at irregular intervals. 
Under the dim light it is impossible to ascertain 
the room proportions...
*/

mooleen.says('Something about this feels very wrong...');
red.says("I couldn't agree more");

randalf.says("Well, it's either destroy that totem and " + 
  "escape aided by magic or go back and fight The Red Legion" +
  "with our bare fists");
rat.says("...and sharp claws");

mooleen.says("Alright, let's get this over with");

/* 
  The group approaches the obelisk under an oppressive
  silence only broken by the sound of boots scraping 
  the sand covered ground.
*/

red.says('Oh shit');
mooleen.says('Oh shit? I thought you ' +
  'were beyond that type of language');

/* 
  Eerie blue and green lights start flaring around the
  group inundating the chamber with light and a ghastly 
  atmosphere. Up and up they go to reveal enormous terraces 
  filled with dark winged figures. A voice suddenly booms 
  within the chamber:
*/

voice.thunders('Let the games begin!');
voice.thunders("In our next game we'll recreate " + 
  "The Fall of the Order of The Red Moon " + 
  "the sacred order of warrior monks" + 
  "whose fierceness still echoes across the centuries");
voice.thunders('I give you: The Last Stand!');

/* 
A thunderous applause mixed with screeches, screams and 
shouts of joy and excitement follows the proclamation.
At the same time 4 humongous iron doors start slowly
opening and row after row of obscene four-legged reptilian 
creatures start emerging from them. Their impossibly huge 
mandibles and terrible wails freeze your blood.
*/

mooleen.says('Oh shit');

voice.thunders('The rules of the game:');
voice.thunders('#1. Fight or Die...');
voice.thunders('#2. You Shall Only Use Types!');
voice.thunders('#3. Only One Shall Remain');

You Shall Only Use Types!

Congratulations on making it to the end of the book! I have a special treat prepared for you as a farewell present: TypeScript! TypeScript has been gaining momentum over the past few years and it is used inside and outside of the .NET world even with popular front-end frameworks such as Angular and React. TypeScript provides the nearest experience to C# that you can find on the web. Enjoy!

JavaScript + Types = Awesome Dev Productivity

TypeScript is a superset of JavaScript that adds type annotations and, thus, static typing on top of JavaScript.

If you are a C# or Java developer you'll feel right at home writing TypeScript. If you are a JavaScript developer or have a background in dynamic programming languages you'll encounter a slightly more verbose version of JavaScript that results in a safer and better developer experience. Either way, you'll be happy to know that everything you've learned about JavaScript thus far also applies to TypeScript, that is, any JavaScript is valid TypeScript.

Any JavaScript is Valid TypeScript

Experiment JavaScriptmancer!!

You can experiment with the examples in this section using the TypeScript playground or downloading the source code from GitHub.

Any bit of JavaScript is valid TypeScript. Let's say that we have the most basic piece of JavaScript code that you can write, a simple variable declaration that represents your reserve of mana:

var manaReserves = 10;
Enter fullscreen mode Exit fullscreen mode

And now let's say that we want to recharge your mana reserves by drinking a magic potion:

function rechargeMana(potion){
  return potion.manaModifier * (Math.floor(Math.rand()*10) + 1);
}
Enter fullscreen mode Exit fullscreen mode

So we go and write the following:

manaReserves += rechargeMana({
  name: 'light potion of mana',
  manaModifier: 1.5 
});
Enter fullscreen mode Exit fullscreen mode

When we execute the piece of code above, it explodes with the following error:

// => Uncaught TypeError: Math.rand is not a function
Enter fullscreen mode Exit fullscreen mode

Which makes sense because there's no such thing as a Math.rand function in JavaScript. It is called Math.random. For some reason I mix this function with a C function that has the same purpose, a slightly different name, and which I used in my student days. Regardless, I make this mistake, again and again.

The code above is a very traditional piece of JavaScript. But it is also completely valid TypeScript, with one difference. Writing the rechargeMana in TypeScript would have automatically resulted in a compiler error that would've read:

Property 'rand' does not exist on type 'Math'.
Enter fullscreen mode Exit fullscreen mode

This would have immediately alerted me to the fact that I'm making a mistake (again), and I would have been able to fix it before executing the program. This is one of the advantages of TypeScript: shorter feedback loops where you can detect errors in your code at compile time instead of at runtime.

Let's expand our previous example and drink another potion:

rechagreMana({
  name: 'Greater Potion of Mana',
  manaModifier: 2
})
Enter fullscreen mode Exit fullscreen mode

Again. A simple typo, a classic mistake in JavaScript that would result in a ReferenceError at runtime, is instantly caught by the TypeScript compiler:

Cannot find name 'rechagreMana'.
Enter fullscreen mode Exit fullscreen mode

As we've seen thus far, the TypeScript compiler that sits between the TypeScript code that you write and the output that runs in the browser can do a lot of things for you on vanilla JavaScript. But it truly shines when you start adding type annotations, that is, when you annotate your JavaScript code with additional bits of information regarding the type of things.

For instance, let's update our original rechargeMana function with some type annotations:

function rechargeMana(potion: { manaModifier : number }) {
  return potion.manaModifier * (Math.floor(Math.random()*10) + 1);
}
Enter fullscreen mode Exit fullscreen mode

The example above contains a type annotation for the potion parameter {manaModifier : number}. This annotation means that the potion parameter is expected to be an object that has a property manaModifier of type number.

The type annotation does several things for us:

  1. It can help the compiler discover errors when the object passed as an argument to rechargeMana doesn't have the expected interface. That is, when it lacks the manaModifier property which is necessary for the function to work.
  2. It can help the compiler discover typos or type errors when you use the potion object within the body of the function.
  3. It gives us statement completion when typing potion inside the rechargeMana function which is a great developer experience1. If you aren't familiar with statement completion it consist on helpful in-editor information that pops up and tells you how you can use an object, like which properties are methods are available, which types are expected for the different parameters, etc.

Let's illustrate 1) with an example. Imagine that in addition to potions of Mana you had potions of Strength:

const potionOfStrength = {
  name: 'Potion of Strength',
  strengthModifier: 3,
  duration: 10
};
Enter fullscreen mode Exit fullscreen mode

At some point in our program we could end up calling this code by mistake:

rechargeMana(potionOfStrength);
Enter fullscreen mode Exit fullscreen mode

Calling the rechargeMana function with a potionOfStrength as argument would result in a runtime error in JavaScript or, perhaps even in an elusive bug since multiplying undefined by a number results in NaN instead of crashing outright.

In TypeScript however, the example above would result in the following compiler error:

// [ts] 
// Argument of type '{ name: string; strengthModifier: number; }' 
// is not assignable to parameter 
//   of type '{ manaModifier: number; }'.
// Property 'manaModifier' is missing 
// in type '{ name: string; strengthModifier: number; }'.
Enter fullscreen mode Exit fullscreen mode

This error would quickly tell me that the potion of strength lacks the required contract to use rechargeMana and lots of tears and frustration would've been saved right then and there. Also take a second to appreciate the quality and precision of the error message above.

So any JavaScript is valid TypeScript. Change your code.js file into code.ts file, run it by the TypeScript compiler and TypeScript will try to infer the most information it can from your code and do its best to help you. Add type annotations on top of that and TypeScript will be able to learn more about your code and intentions, and provide you with better support.

So, What Are The Advantages and Disadvantages of TypeScript?

By enhancing your JavaScript with new features, type annotations and static typing TypeScript provides these advantages:

  • Better error detection. TypeScript can do static analysis of your code and reveal errors before running the actual code. This provides a much shorter feedback loop so that you can fix these errors as soon as they happen inside your editor and not after they hit production.
  • Better tooling and developer productivity. The rich type information can be used by editors and IDEs to provide great tooling to enhance your developer productivity like in-editor compiler warnings, statement completion, safe refactorings, inline documentation, etc... Visual Studio Code is a text editor that has awesome TypeScript support out of the box.
  • Great API discoverability. Using statement completion provided by type annotations is an outstanding way to discover about new APIs right inside your editor.
  • Write more intentional code. TypeScript type annotations and additional features like access level keywords allow you to constrain how the APIs that you design are meant to be used. This allows you to write more intentional code.
  • ESnext features. TypeScript supports a lot of ESnext features like class members, decorators and async/await.
  • Additional TypeScript Features. In addition to JavaScript and ESnext features TypeScript has a small number of features that are not in the ECMA-262 specification which add a lot to the language like property access levels and parameter properties.
  • Works with third-party libraries. Using type annotations in your application code is awesome but what about all the third-party libraries that you use and are reference throughout your application code? How does TypeScript interact with them? Particularly, what happens when these libraries aren't written in TypeScript? In the worst case scenario TypeScript treats objects it doesn't know as of type any which basically means "this object can have any shape so just behave as you would in JavaScript and don't make any assumptions". More often, third-party libraries either come with declaration files that provide typing information for TypeScript or you can find these declaration files through the DefinitelyTyped project a repository of TypeScript type definitions. This means that you'll be able to enjoy the same level of TypeScript support (or even greater) for third-party libraries that you do for your own code.
  • Great for large-scale applications and teams. TypeScript excels at supporting multiple teams with large-scale applications. The type annotations and the TypeScript compiler are awesome at catching breaking changes, subtle bugs and with new APIs discoverability.

On the minus side:

  • TypeScript requires a transpilation step. TypeScript code is not supported as-is in any browser. In order to be able to write your applications in TypeScript you need to setup some sort of build pipeline to transpile your TypeScript code into a version of JavaScript that can run in the browser. Fortunately, there is great support for this in the open source community and you can find great integrations for TypeScript in the most popular frameworks and build tools.
  • You need to learn type annotations syntax and related artifacts. The type annotations, their syntax and related artifacts like interfaces, generics, etc... add more cognitive load and an extra degree of complexity on top of all you need to know to write JavaScript applications.
  • It is verbose. The addition of type annotations makes your JavaScript code more verbose (call(person:Person)) which can be quite aesthetically unpleasing (particularly at first). The TypeScript compiler does a great job at inferring types and reducing the amount of type annotations you need to write to the minimum but to make the most out of TypeScript you'll need to add a fair amount of type annotations yourself.
  • It sometimes falls out of line with the ECMAScript standard. Bringing ESnext features to you today, although awesome, can have its drawbacks. Implementing ESnext features before they've been formalized can lead to TypeScript breaking with the standards as it happened with modules. Fortunately, the core philosophy of TypeScript being a superset of JavaScript led the TypeScript team to implement support for ES6 modules and to deprecate the non-standard version. This is a great indicator of TypeScript's allegiance to JavaScript but still bears consideration when adopting ESnext features.

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 chapter. 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:

$ npm install -g typescript
Enter fullscreen mode Exit fullscreen mode

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

$ tsc -v
Version 2.4.2
Enter fullscreen mode Exit fullscreen mode

And accessing the TypeScript compiler help:

$ tsc -h
Version 2.4.2
Syntax:   tsc [options] [file ...]

Examples: tsc hello.ts
          tsc --outFile file.js file.ts
          tsc @args.txt
Enter fullscreen mode Exit fullscreen mode

I will use Visual Studio Code during these examples but you're welcome to use any editor that you prefer.

Typing this command below will create a new TypeScript file called hello-wizard.ts and will open it on Visual Studio Code:

$ code hello-wizard.ts
Enter fullscreen mode Exit fullscreen mode

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

function sayHello(who: string) : void {
  console.log(`Hello ${who}! I salute you JavaScript-mancer!`);
}
Enter fullscreen mode Exit fullscreen mode

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:

sayHello(42);
// => [ts] Argument of type '42' is not assignable 
//         to parameter of type 'string'.
Enter fullscreen mode Exit fullscreen mode

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

sayHello('<Your name here>');
Enter fullscreen mode Exit fullscreen mode

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:

$ tsc hello-world.ts
Enter fullscreen mode Exit fullscreen mode

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:

function sayHello(who) {
  console.log("Hello " + who + "! I salute you JavaScript-mancer!");
}
sayHello('<Your name here>');
Enter fullscreen mode Exit fullscreen mode

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

$ node hello-world.js
Hello <Your name here>! I salute you JavaScript-mancer!
Enter fullscreen mode Exit fullscreen mode

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:

$ tsc -w hello-world.ts
10:55:11 AM - Compilation complete. Watching for file changes.
Enter fullscreen mode Exit fullscreen mode

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.

Visual Studio Code Works Great With TypeScript!

If you want to learn more about how to have a great setup in Visual Studio Code with TypeScript I recommend you to take a look at this guide.

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 brings you a lot of ESnext features

A lot of the features that we'll see in this section are ESnext features that are proposals at different levels of maturity. You can find more information about all proposals currently under consideration in the TC39 ECMA-262 GitHub repository.

Some of these features are available also when using Babel with experimental flags. The fact that you have a team within Microsoft maintaining TypeScript gives you a lot of confidence when using these features within TypeScript.

TypeScript Classes

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

Instead of writing your classes like this:

// ES6 class
class Gladiator {
  constructor(name, hitPoints){
    this.name = name;
    this.hitPoints = hitPoints;
  }
  toString(){
    return `${this.name} the gladiator`
  }
}
Enter fullscreen mode Exit fullscreen mode

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

class Gladiator {
  name: string;
  hitPoints: number;

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

  toString(){
    return `${this.name} the gladiator`
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

class SleekGladiator {
  constructor(public name: string, 
              public hitPoints: number){}

  toString(){
    return `${this.name} the gladiator`
  }
}
Enter fullscreen mode Exit fullscreen mode

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 ES6 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 chapters.

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

class FixedGladiator {

  constructor(readonly name: string,
              public hitPoints: number){}

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

}
Enter fullscreen mode Exit fullscreen mode

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

const maximo = new FixedGladiator('Maximo', 5000);

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

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:

var FixedGladiator = (function () {

  function FixedGladiator(name, hitPoints) {
    this.name = name;
    this.hitPoints = hitPoints;
  }

  FixedGladiator.prototype.toString = function () {
    return this.name + ", the gladiator";
  };

  return FixedGladiator;
}());
Enter fullscreen mode Exit fullscreen mode

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 chapters 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 chapter 6. White Tower Summoning Enhanced: The Marvels of ES6 Classes to showcase data hiding using closures:

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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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

The equivalent of this class in TypeScript looks like this:

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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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

const conan = new PrivateBarbarian("shy Conan");
// const privateWeapons = conan.weapons;
// => [ts] Property 'weapons' is private and 
//         only accessible within class 'PrivateBarbarian'.
Enter fullscreen mode Exit fullscreen mode

If you look back and compare both approaches I think that you'll agree with me that the TypeScript syntax reads better than the ES6 counterpart. Having all methods within the body of the class is more consistent and easier to understand than having methods split in two separate places.

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, I can see it being an issue for library creators that create their library using TypeScript and make it accessible to consumers that are using vanilla JavaScript.

Why Do I Get A TypeScript Error When Writing An ES6 class? Isn't It Valid JavaScript?

Excellent question! When you type the code example with the ES6 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. What? I thought that every piece of JavaScript was valid TypeScript and this is perfectly valid ES6 code. What's happening? Have I been living a lie?

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 the transpilation and emitting correct JavaScript code (this is the case for the issue we've just discovered regarding ES6 classes).
  • 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.

So in our ES6 example, TypeScript understands your code as valid ES6 and will be able to transpile your code into JavaScript but according to TypeScript's type system you should refactor your class and move the class members inside the class body.

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:

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)`);
  }
};
Enter fullscreen mode Exit fullscreen mode

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:

const schoolsOfElementalMagic = {
  fire: 'fire',
  water: 'water',
  air: 'air',
  earth: 'earth'
};
Enter fullscreen mode Exit fullscreen mode

And now we can rewrite our previous example:

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)`);
  }
};
Enter fullscreen mode Exit fullscreen mode

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 from writing type: 'banana' inside your spell.

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

enum SchoolsOfMagic {
  Fire,
  Water,
  Air,
  Earth
}
Enter fullscreen mode Exit fullscreen mode

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 SchoolsOfMagic we just created:

// now we can define a Spell interface
interface Spell {
  type: SchoolsOfMagic,
  damage: number,
  cast(target: any);
}
Enter fullscreen mode Exit fullscreen mode

Interfaces?

Interfaces are another new feature in TypeScript. They allow you to define arbitrary types that result in more intentional code and enrich your developer experience. We'll learn more about interfaces later in this chapter.

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.

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)`);
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

// 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'.
Enter fullscreen mode Exit fullscreen mode

When transpiled to JavaScript enums result in the following code:

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 = {}));
Enter fullscreen mode Exit fullscreen mode

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

// 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 "Fire"
Enter fullscreen mode Exit fullscreen mode

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:

// 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
}
Enter fullscreen mode Exit fullscreen mode

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:

const enum SchoolOfMagic {
  Fire,
  Water,
  Air,
  Earth
}
Enter fullscreen mode Exit fullscreen mode

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 on a series of specific valid strings. An equivalent for our schools of magic could look like this:

type SchoolsOfMagic = "fire" | "earth" | "air" | "water";
Enter fullscreen mode Exit fullscreen mode

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

interface Spell {
  type: SchoolsOfMagic,
  damage: number,
  cast(target: any);
}
Enter fullscreen mode Exit fullscreen mode

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

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'.
Enter fullscreen mode Exit fullscreen mode

Object Spread and Rest

In JavaScript-mancy: Getting Started we saw rest parameters and the spread operator brought by ES6.

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

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!');
}
Enter fullscreen mode Exit fullscreen mode

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

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!');
}
Enter fullscreen mode Exit fullscreen mode

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 an array and expands it into its compounding items.

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

let knownFoesLevel1 = ['rat', 'rabbit']
let newFoes = ['globin', 'ghoul'];
let knownFoesLevel2 = [...knownFoesLevel1, ...newFoes];
Enter fullscreen mode Exit fullscreen mode

Or cloning them:

let foes = ['globin', 'ghoul'];
let clonedFoes = [...foes];
Enter fullscreen mode Exit fullscreen mode

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 chapters 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:

function Wizard(element, mana, name, hp){
  let wizard = {element, 
                mana, 
                name, 
                hp};
  Object.assign(wizard, 
               canBeIdentifiedByName,
               canCastSpells);
  return wizard;
}
Enter fullscreen mode Exit fullscreen mode

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

function Wizard(element, mana, name, hp){
  let wizard = {element, 
                mana, 
                name, 
                hp};

  // now we use object spread
  return {...wizard, 
          ...canBeIdentifiedByName,
          ...canCastSpells
         };
}
Enter fullscreen mode Exit fullscreen mode

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 ES6 rest parameters and are particularly helpful together with ES6 destructuring.

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

let [first, second, ...rest] = ['dragon', 'chimera', 'harpy', 'medusa'];
console.log(first); // => dragon
console.log(second); // => chimera
console.log(rest); // => ['harpy', 'medusa']
Enter fullscreen mode Exit fullscreen mode

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

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'}
Enter fullscreen mode Exit fullscreen mode

And There's More!

There's a lot more features in TypeScript that expand on ES6 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.

Type Annotations In TypeScript

Type annotations are TypeScript's bread and butter and provide yet a new level of meta-programming in JavaScript: type meta-programming. Type annotations give you the ability to create a better developer experience for you and your team by ways of shorter feedback loops, compile time errors and API discoverability.

Type annotations in TypeScript don't stop at simple primitive types like string or number. You can specify the type of arrays:

// An array of strings
let saddleBag: string[] = [];
saddleBag.push('20 silvers');
saddleBag.push('pair of socks');

saddleBag.push(666);
// => [ts] Argument of type '666' is not assignable 
//         to parameter of type 'string'.
Enter fullscreen mode Exit fullscreen mode

and tuples:

// A tuple of numbers
let position : [number, number];
position = [1, 1];
position = [2, 2];

// position = ['orange', 'delight'];
// => [ts] Type '[string, string]' is not 
//    assignable to type '[number, number]'.
//    Type 'string' is not assignable to type 'number'.
Enter fullscreen mode Exit fullscreen mode

functions:

// a predicate function that takes numbers and returns a boolean
let predicate: (...args: number[]) => boolean;
predicate = (a, b) => a > b
console.log(`1 greated than 2? ${predicate(1, 2)}`);
// => 1 greated than 2? false

predicate = (text:string) => text.toUpperCase();
// => [ts] Type '(text: string) => string' is not assignable 
//         to type '(...args: number[]) => boolean'.
//     Types of parameters 'text' and 'args' are incompatible.
//     Type 'number' is not assignable to type 'string'.
Enter fullscreen mode Exit fullscreen mode

and even objects:

function frost(minion: {hitPoints: number}) {
  const damage = 10;
  console.log(`${minion} is covered in frozy icicles (- ${damage} hp)`);
  minion.hitPoints -= damage;
}
Enter fullscreen mode Exit fullscreen mode

The {hitPoints: number} represents and object that has a hitPoints property of type number. We can cast a frost spell on a dangerous foe that must comply with the required contract - that of having a hitPoints property:

const duck = {
  toString(){ return 'a duck';}, 
  hitPoints: 100
};

frost(duck);
// => a duck is covered in frozy icicles (-10hp)
Enter fullscreen mode Exit fullscreen mode

If the object frozen doesn't satisfy the requirements, TypeScript will alert us instantly:

const theAir = {
    toString(){ return 'air';}
};
frost(theAir);
// => [ts] Argument of type '{ toString(): string; }' 
//    is not assignable to parameter 
//      of type '{ hitPoints: number; }'.
// Property 'hitPoints' is missing in type '{ toString(): string; }'.
Enter fullscreen mode Exit fullscreen mode

An even better way to annotate objects is through interfaces.

TypeScript Interfaces

Interfaces are reusable and less verbose than straight object type annotations. A Minion interface could be described as follows:

interface Minion {
    hitPoints: number;
}
Enter fullscreen mode Exit fullscreen mode

We could use this new interface to update our frost function:

function frost(minion: Minion){
  const damage = 10;
  console.log(`${minion} is covered in frozy icicles (-${damage} hp)`);
  minion.hitPoints -= damage;
}
Enter fullscreen mode Exit fullscreen mode

Looks nicer, doesn't it? An interesting fact about interfaces is that they are entirely a TypeScript artifact whose only application is within the realm of type annotations and the TypeScript compiler. Because of that, interfaces are not transpiled into JavaScript. If you transpile the code above you'll be surprised to see that the resulting JavaScript has no mention of Minion:

function frost(minion) {
    var damage = 10;
    console.log(minion + " is covered in frozy icicles (-" + damage + " hp)");
    minion.hitPoints -= damage;
}
Enter fullscreen mode Exit fullscreen mode

This points to the fact that interfaces are a lightweight approach to adding type annotations to your codebase, reaping the benefits during development without having any negative impact in the code that runs on the browser.

Let's test our new frost function and the Minion interface with different types of arguments and see how they behave. Bring on the duck from our previous example!

// const duck = {
//  toString(){ return 'duck';}, 
//  hitPoints: 100
//  };
frosty(duck);
// => duck is covered in frozy icicles (-10hp)
Enter fullscreen mode Exit fullscreen mode

That seems to work perfectly. If we try with a class that represents a Tower and has a hitPoints and a defense property it seems to work as well:

class Tower {
    constructor(public hitPoints=500, public defense=100){}
    toString(){ return 'a mighty tower';}
}
const tower = new Tower();

frosty(tower);
// => a mighty tower is covered in frozy icicles (-10hp)
Enter fullscreen mode Exit fullscreen mode

And so does a simple object literal with the hitPoints property:

frosty({hitPoints: 100});
// => [object Object] is covered in frozy icicles (-10hp)
Enter fullscreen mode Exit fullscreen mode

However if we use an object literal that has another property in addition to hitPoints the compiler throws an error:

frosty({hitPoints: 120, toString(){ return 'a bat';}})
// => doesn't compile
// => Argument of type '{ hitPoints: number; toString(): string; }' 
//    is not assignable to parameter of type 'Minion'.
//  Object literal may only specify known properties, 
//  and 'toString' does not exist in type 'Minion'.
Enter fullscreen mode Exit fullscreen mode

The error message seems to be very helpful. It says that with object literals I may only specify known properties and that toString doesn't exist in Minion. So what happens if I store the object literal in a variable aBat?

let aBat = {
    hitPoints: 120, 
    toString(){ return 'a bat';}
};
frosty(aBat);
// => a bat is covered in frozy icicles (-10hp)
Enter fullscreen mode Exit fullscreen mode

It works! Interesting, from these experiments it looks like TypeScript will consider a Minion to be any object that satisfies the contract specified by the interface, that is, to have a hitPoints property of type number.

However, it looks like when you use an object literal TypeScript has a somewhat more strict set of rules and it expects an argument that exactly matches the Minion interface. So what is a Minion exactly? When TypeScript encounters an arbitrary object, how does it determine whether it is a Minion or not?

It follows the rules of structural typing.

Structural Typing

Structural typing is a type system where type compatibility and equivalence are determined by the structure of the types being compared, that is, their properties.

For instance, following structural typing all of the types below are equivalent because they have the same structure (the same properties):

// an interface
interface Wizard {
  hitPoints: number;
  toString(): string;
  castSpell(spell:any, targets: any[]);
}

// an object literal
const bard = {
  hitPoints: 120,
  toString() { return 'a bard';},
  castSpell(spell: any, ...targets: any[]){
    console.log(`${this} cast ${spell} on ${targets}`);
    spell.cast(targets);
  }
}

// a class
class MagicCreature {
  constructor(public hitPoints: number){}
  toString(){ return "magic creature";}
  castSpell(spell: any, ...targets: any[]){
    console.log(`${this} cast ${spell} on ${targets}`);
    spell.cast(targets);
  }
}
Enter fullscreen mode Exit fullscreen mode

Which you can verify using this snippet of code:

let wizard: Wizard = bard;
let anotherWizard: Wizard = new MagicCreature(120);
Enter fullscreen mode Exit fullscreen mode

In contrast, languages like C# or Java have what we call a nominal type system. In nominal type systems, type equivalence is based on the names of types and explicit declarations, where a MagicCreature is a Wizard, if and only if, the class implements the interface explicitly.

Structural typing is awesome for JavaScript developers because it behaves very much like duck typing that is such a core feature to JavaScript object-oriented programming model. It is still great for C#/Java developers as well because they can enjoy C#/Java features like interfaces, classes and compile-time feedback but with a higher degree of freedom and flexibility.

There's still one use case that doesn't fit the structural typing rule we just described. If you remember the examples from the previous section, object literals seem to be an exception to the structural typing rule:

frosty({hitPoints: 120, toString(){ return 'a bat';}})
// => doesn't compile
// => Argument of type '{ hitPoints: number; toString(): string; }' 
//    is not assignable to parameter of type 'Minion'.
//  Object literal may only specify known properties, 
//  and 'toString' does not exist in type 'Minion'.
Enter fullscreen mode Exit fullscreen mode

Why does that happen? It happens in order to prevent developer mistakes.

The TypeScript compiler designers considered that using object literals like this can be prone to errors and mistakes (like typos, imagine writing hitPoitns instead of hitPoints). That is why when using object literals in this fashion the TypeScript compiler will be extra diligent and perform excess property checking. Under this special mode TypeScript will be inclined to be extra careful and will flag any additional property that the function frosty doesn't expect. Everything in the hopes of helping you avoid unnecessary mistakes.

If you are sure that your code is correct, you can quickly tell the TypeScript compiler that there's no problem by explicitly casting the object literal to the desired type or storing it in a variable as we saw earlier:

frosty({hitPoints: 120, toString(){ return 'a bat';}} as Minion);
// => a bat is covered in frozy icicles (-10hp)
Enter fullscreen mode Exit fullscreen mode

Notice the as Minion? That's a way we can tell TypeScript that the object literal is of type Minion. This is another way:

frosty((<Minion>{hitPoints: 120, toString(){ return 'a bat';}}));
// => a bat is covered in frozy icicles (-10hp)
Enter fullscreen mode Exit fullscreen mode

TypeScript Helps You With Type Annotations

Another interesting facet of TypeScript are its type inference capabilities. Writing type annotations not only results in more verbose code but it's also additional work that you need to do. In order to minimize the amount of work that you need to put in to annotate your code, TypeScript will do its best to infer the types used from the code itself. For instance:

const aNumber = 1;
const anotherNumber = 2 * aNumber;

// aNumber: number
// anotherNumber:number
Enter fullscreen mode Exit fullscreen mode

In this code sample we haven't specified any types. Regardless, TypeScript knows without the shadow of a doubt that the aNumber variable is of type number, and by evaluating anotherNumber it knows that it's also of type number. Likewise we can write the following:

const double = (n: number) => 2*n;
// double: (n:number) => number
Enter fullscreen mode Exit fullscreen mode

And TypeScript will know that the function double returns a number.

From Interfaces to Classes

So far we've seen how you can use type annotations in the form of primitive types, arrays, object literals and interfaces. All of these are TypeScript specific artifacs that disappear when you transpile your TypeScript code to JavaScript. We've also seen how TypeScript attempts to infer types from your code so that you don't need to expend unnecessary time annotating your code.

Then we have classes. Classes are a ES6/TypeScript feature that we can use to describe a domain model entity in structure and behavior, which contain a specific implementation, and which also serve as a type annotation.

In previous sections we defined an interface Minion that represented a thing with a hitPoints property. We can do the same with a class:

class ClassyMinion {
  constructor(public hitPoints: number) {}
}
Enter fullscreen mode Exit fullscreen mode

And create a new classyFrost function to use this class as the argument type:

function classyFrost(minion: ClassyMinion){
  const damage = 10;
  console.log(`${minion} is covered in frozy icicles (-${damage} hp)`)
  minion.hitPoints -= damage;
}
Enter fullscreen mode Exit fullscreen mode

We can use this function with our new ClassyMinion class and even with the previous aBat and bard variables because following the rules of structural typing all of these types are equivalent:

classyFrosty(new ClassyMinion());
// => a classy minion is covered in frozy icicles (-10hp)
classyFrosty(aBat);
// => a bat is covered in frozy icicles (-10hp)
classyFrosty(bard);
// => a bard is covered in frozy icicles (-10hp)
Enter fullscreen mode Exit fullscreen mode

Normally we would have the class implement the desired interface. For instance:

class ClassyMinion implements Minion {
  constructor(public hitPoints: number) {}
}
Enter fullscreen mode Exit fullscreen mode

This wouldn't make a change in how this class is seen from a structural typing point of view but it does improve our developer experience. Adding the implements Minion helps TypeScript tell us whether we have implemented an interface correctly or if we're missing any properties or methods. This may not sound like much in a class with one single property but it's increasingly helpful as our classes become more meaty.

In general, the difference between using a class and using an interface is that the class will result in a real JavaScript class when transpiled to JavaScript (although it could be a constructor/prototype pair depending on the JavaScript version your are targeting).

For instance, the class above will result in the following JavaScript in our current setup:

var ClassyMinion = (function () {
    function ClassyMinion(hitPoints) {
        if (hitPoints === void 0) { hitPoints = 100; }
        this.hitPoints = hitPoints;
    }
    ClassyMinion.prototype.toString = function () {
        return 'a classy minion';
    };
    return ClassyMinion;
}());
Enter fullscreen mode Exit fullscreen mode

This makes sense because, unlike an interface which is a made up artifact used only in the world of TypeScript type annotations, a class is necessary to run your program.

When do you use interfaces and when do you use classes then? Let's review what both of these constructs do and how they behave:

  • Interface: Describes shape and behavior. It's removed during transpilation process.
  • Class: Describes shape and behavior. Provides a specific implementation. It's transpiled into JavaScript

So both interfaces and class describe the shape and behavior of a type. And additionally, classes provide a concrete implementation.

In the world of C# or Java, following the dependency inversion principle we'd advice to prefer using interfaces over classes when describing types. That would afford us a lot of flexibility and extensibility within our programs because we would achieve a loosely coupled system where concrete types don't know about each other. We then would be in a position to inject diverse concrete types that would fulfill the contract defined by the interfaces. This is a must in statically typed languages like C# or Java because they use a nominal type system. But what about TypeScript?

As we mentioned earlier, TypeScript uses a structural type system where types are equivalent when they have the same structure, that is, the same members. In light of that, you could say that it doesn't really matter if we use interfaces or classes to denote types. If interfaces, classes or object literals share the same structure, they'll be equally treated, so why would we need to use interfaces in TypeScript? Here are some guidelines that you can follow when you consider using interfaces vs classes:

  1. The single responsibility is a great rule of thumb to decrease the complexity of your programs. Applying the single responsibility to the interface vs class dilemma we can arrive to use interfaces for types and classes for implementations. Interfaces provide a very concise way to represent the shape of a type, whilst classes intermingle both the shape and the implementation which can make it hard to ascertain what the shape of a type is by just looking at a class.
  2. interfaces give you more flexibility than classes. Because a class contains a specific implementation, it is, by its very nature, more rigid than an interface. Using interfaces we can capture finely grained details or bits of behavior that are common between classes.
  3. interfaces are a lightweight way to provide type information to data that may be foreign to your application like data coming from web services
  4. For types with no behavior attached, types that are merely data, you can use a class directly. Using an interface in this case will often be overkill and unnecessary. Using a class will ease object creation via the constructor.

So, in general, the same guidelines that we follow regarding interfaces in statically typed languages like C# and Java also apply to TypeScript. Prefer to use interfaces to describe types and use classes for specific implementations. If the type is just data with no behavior you may consider using a class on its own.

Advanced Type Annotations

In addition to what we've seeing thus far TypeScript provides more mechanisms to express more complex types in your programs. The idea is that, whichever JavaScript construct or pattern you use, you should be able to express its type via type annotations and provide helpful type information for you and other developers within your team.

Some examples of these advanced type annotations are:

  • Generics
  • Intersection and Union Types
  • Type Guards
  • Nullable Types
  • Type Aliases
  • String-literal Types

Let's take a look at each of them, why they are needed and how to use them.

Generics

Generics is a common technique used in statically typed programming languages like C# and Java to generalize the application of a data structure or algorithm to more than one type.

For instance, instead of having a separate Array implementation for each different type: NumberArray, StringArray, ObjectArray, etc:

interface NumberArray {
  push(n: number);
  pop(): number;
  [index: number]: number;
  // etc
}

interface StringArray {
  push(s: string);
  pop(): string;
  [index: number]: string;
  // etc
}

// etc...
Enter fullscreen mode Exit fullscreen mode

We use generics to describe an Array of an arbitrary type T:

// note that `Array<T>` is already a built-in type in TypeScript
interface Array<T>{
  push(s: T);
  pop(): T;
  [index: number]: T;
  // etc
}
Enter fullscreen mode Exit fullscreen mode

We can now reuse this single type definition by selecting a type for T:

let numbers: Array<number>;
let characters: Array<string>;
// and so on...
Enter fullscreen mode Exit fullscreen mode

And just like we used generics with interfaces, we can use them with classes:

class Cell<T> {
  private prisoner: T;

  inprison(prisoner: T) { 
    this.prisoner = item; 
  }

  free(): T { 
    const prisoner = this.prisoner; 
    this.prisoner = undefined;
    return prisoner;
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, you can constrain the type T to only a subset of types. For instance, let's say that a particular function only makes sense within the context of Minion. You can write:

interface ConstrainedCell<T extends Minion>{
  inprison(prisoner: T);
  free(): T;
}
Enter fullscreen mode Exit fullscreen mode

And now this will be a perfectly usable box:

let box: ConstrainedCell<MagicCreature>;
Enter fullscreen mode Exit fullscreen mode

But this won't because the type T doesn't match the Minion interface:

let box: ConstrainedCell<{name: string}>;
// => [ts] Type '{ name: string; }' 
//    does not satisfy the constraint 'Minion'.
//    Property 'hitPoints' is missing in type '{ name: string; }'.
Enter fullscreen mode Exit fullscreen mode

Intersection and Union Types

We've seen primitive types, interfaces, classes, generics, a lot of different ways to provide typing information but flexible as these may be, there's still a use case which they have a hard time covering: Mixins.

When using mixins the resulting object is a mix of other different objects. The type of this resulting object is not a known type in its own right but a combination of existing types.

For instance, let's go back to the Wizard example that we had earlier:

function Wizard(element, mana, name, hp){
  let wizard = {element, 
                mana, 
                name, 
                hp};

  // now we use object spread
  return {...wizard, 
          ...canBeIdentifiedByName,
          ...canCastSpells
         };
}
Enter fullscreen mode Exit fullscreen mode

We can decompose this into separate elements:

interface WizardProps{
  element: string;
  mana: number;
  name: string;
  hp: number;
}

interface NameMixin {
  toString(): string;
}

interface SpellMixin {
  castsSpell(spell:Spell, target: Minion);
}
Enter fullscreen mode Exit fullscreen mode

How can we define the resulting Wizard type that is the combination of WizardProps, NameMixin and SpellMixin? We use Intersection Types. An Intersection Type allows us to define types that are the combination of other types. For instance, we could represent our Wizard using the following type annotation:

WizardProps & NameMixin & SpellMixin
Enter fullscreen mode Exit fullscreen mode

And we could use it as a return type of our factory function:

let canBeIdentifiedByName: NameMixin = {
  toString(){ return this.name; }
};

let canCastSpells: SpellMixin = {
  castsSpell(spell:Spell, target:Minion){
    // cast spell
  }
}

function WizardIntersection(element: string, mana: number, 
                            name : string, hp: number): 
         WizardProps & NameMixin & SpellMixin {
  let wizard: WizardProps = {element, 
                mana, 
                name, 
                hp};

  // now we use object spread
  return {...wizard, 
          ...canBeIdentifiedByNameMixin,
          ...canCastSpellsMixin
         };
}

const merlin = WizardIntersection('spirit', 200, 'Merlin', 200);
// merlin.steal(conan);
// => [ts] Property 'steal' does not exist 
//    on type 'WizardProps & NameMixin & SpellMixin'.
Enter fullscreen mode Exit fullscreen mode

In the same way that we have a Intersection Types that result in a type that is a combination of other types we also have the ability to make a type that can be any of a series of types, that is, either string or number or other type. We call these types Union Types. They are often used when you have overloaded functions or methods that may take a parameter with varying types.

Take a look at the following function that raises an skeleton army:

function raiseSkeleton(numberOrCreature){
  if (typeof numberOrCreature === "number"){
    raiseSkeletonsInNumber(numberOrCreature);
  } else if (typeof numberOrCreature === "string") {
    raiseSkeletonCreature(numberOrCreature);
  } else {
    console.log('raise a skeleton');
  }

  function raiseSkeletonsInNumber(n){
    console.log('raise ' + n + ' skeletons');
  }
  function raiseSkeletonCreature(creature){
    console.log('raise a skeleton ' + creature);
  };
}
Enter fullscreen mode Exit fullscreen mode

Depending on the type of numberOrCreature the function above can raise skeletons or skeletal creatures:

raiseSkeleton(22);
// => raise 22 skeletons

raiseSkeleton('dragon');
// => raise a skeleton dragon
Enter fullscreen mode Exit fullscreen mode

We can add some TypeScript goodness to the raiseSkeletonTS function using union types:

function raiseSkeletonTS(numberOrCreature: number | string){
  if (typeof numberOrCreature === "number"){
    raiseSkeletonsInNumber(numberOrCreature);
  } else if (typeof numberOrCreature === "string") {
    raiseSkeletonCreature(numberOrCreature);
  } else {
    console.log('raise a skeleton');
  }

  function raiseSkeletonsInNumber(n: number){
    console.log('raise ' + n + ' skeletons');
  }
  function raiseSkeletonCreature(creature: string){
    console.log('raise a skeleton ' + creature);
  };
}
Enter fullscreen mode Exit fullscreen mode

The number | string is a Union Type that allows numberOrCreature to be of type number or string. If we by mistake use something else, TypeScript has our backs:

raiseSkeletonTS(['kowabunga'])
// => [ts] Argument of type 'string[]' is not assignable 
//         to parameter of type 'string | number'.
// Type 'string[]' is not assignable to type 'number'.
Enter fullscreen mode Exit fullscreen mode

Type Guards

Union types raise a special case inside the body of a function. If numberOrCreature can be a number or a string, how does TypeScript know which methods are supported? Number methods differ greatly from String methods, so what is allowed?

When TypeScript encounters a union type as in the function above, by default, you'll only be allowed to use methods and properties that are available in all the types included. It is only when you do a explicit conversion or include a type guard that TypeScript will be able to determine the type in use and be able to assist you. Fortunately, TypeScript will recognize type guards that are common JavaScript patterns, like the typeof that we used in the previous example. After performing a type guard if (typeof numberOrCreature === "number") TypeScript will know with certainty that whatever piece of code you execute inside that if block the numberOrCreature will be of type number.

Type Aliases

Another helpful mechanism that works great with Intersection and Union Types are Type Aliases. Type Aliases allow you to provide arbitrary names (aliases) to refer to other types. Tired of writing this Intersection Type?

WizardProps & NameMixin & SpellMixin
Enter fullscreen mode Exit fullscreen mode

You can create an alias Wizard and use that instead:

type Wizard = WizardProps & NameMixin & SpellMixin;
Enter fullscreen mode Exit fullscreen mode

This alias will allow you to improve the Wizard factory from previous examples:

function WizardAlias(element: string, mana: number, 
                name : string, hp: number): Wizard {
  let wizard: WizardProps = {element, 
                mana, 
                name, 
                hp};

  // now we use object spread
  return {...wizard, 
          ...canBeIdentifiedByNameMixin,
          ...canCastSpellsMixin
         };
}
Enter fullscreen mode Exit fullscreen mode

More Type Annotations!

Although I've tried to be quite comprehensive in covering TypeScript within this final chapter of the book, there's plenty more features and interesting things that I won't be able to cover unless I write a complete book on TypeScript.

If you are interested into learning more about all the cool stuff that you can do with TypeScript type annotations then let me insist once more in the TypeScript handbook and at the release notes.

Working with TypeScript in Real World Applications

So TypeScript is great, it gives you lots of great new features on top of ES6 and an awesome developer experience via type annotations, but how do you start using it in real world applications?

The good news is that you'll rarely need to create a TypeScript setup from scratch. The most popular front-end frameworks have built-in support for TypeScript. For instance, TypeScript is the main language of choice for Angular and starting a new project with Angular and TypeScript is as easy as using the Angular cli and typing:

$ ng new my-new-app
Enter fullscreen mode Exit fullscreen mode

Likewise using React and the Create React App tool (also known as CRA) starting a React project with TypeScript takes only typing4:

$ create-react-app my-new-app --scripts-version=react-scripts-ts
Enter fullscreen mode Exit fullscreen mode

If you use any of these options above you're good to go. In either case a new app will be bootstrapped for you and you'll be able to start building your Angular or React app with TypeScript.

On the other hand, if you, for some reason, need to start from scratch you'll find that there are TypeScript plugins for the most common task managers or module bundlers like grunt, gulp or webpack. While integrating TypeScript into your tool chain there's one additional step that you may need to take in order to configure the TypeScript compiler: setting up your tsconfig file.

The tsconfig.json File

The tsconfig.json file contains the TypeScript configuration for your project. It tells the TypeScript compiler about all the details it needs to know to compile your project like:

  • Which files to transpile
  • Which files to ignore
  • Which version of JavaScript to use as a target of the transpilation
  • Which module system to use in the output JavaScript
  • How strict the compiler should be. Should it allow implicit any? Should it perform strict null checks?
  • Which third-party libraries types to load

If you don't specify part of the information, the TypeScript compiler will try to do its best. For instance, not specifying any files to transpile will prompt the TypeScript compiler to transpile all TypeScript files (*.ts) within the project folder. Not specifying any third-party types will lead the TypeScript compiler to look for type definition files within your project (f.i. within ./node_modules/@types).

This is an example tsconfig.json from the TypeScript documentation that can give you an idea:

{
    "compilerOptions": {
        "module": "system",
        "noImplicitAny": true,
        "removeComments": true,
        "preserveConstEnums": true,
        "outFile": "../../built/local/tsc.js",
        "sourceMap": true
    },
    "include": [
        "src/**/*"
    ],
    "exclude": [
        "node_modules",
        "**/*.spec.ts"
    ]
}
Enter fullscreen mode Exit fullscreen mode

For a full reference of all the available options take a look at the TypeScript documentation.

In This Chapter's Examples We Didn't Use A tsconfig. How Come?

The TypeScript compiler tsc has two different modes of operation: with or without input files.
When you don't specify input files while executing tsc the TypeScript compiler will try to find an available tsconfig.json file with its configuration.
When you do specify input files the TypeScript compiler will ignore tsconfig.json. That is why in previous sections we didn't need to define a tsconfig.json file when we run tsc hello-wizard.ts.

TypeScript and Third Party Libraries

Starting from TypeScript 2.0 installing type declarations for third party libraries is as easy as installing any other library via npm.

Imagine that you want to take advantage of ramda.js a library with helpful utility functions with a strong functional programming flavor that we'll see in-depth in the functional programming tome of JavaScript-mancy.

You can add the library to your TypeScript project using npm:

# create package.json
$ npm init 

# install ramda and save dependency
$ npm install --save ramda
Enter fullscreen mode Exit fullscreen mode

And you can install the type declarations for that library using @types/<name-of-library-in-npm>:

$ npm install --save-dev @types/ramda
Enter fullscreen mode Exit fullscreen mode

Now when you start working on your project within Visual Studio Code or your editor of choice you should get full type support when using ramda.js. Try writing the snippet below and verify how TypeScript helps you along the way:

import { add } from 'ramda';

const add5 = add(5);

console.log(`5 + 5: ${add5(5)}`);
console.log(`5 + 10: ${add5(1)}`);
Enter fullscreen mode Exit fullscreen mode

All these type definitions come from the DefinitelyTyped project and are pushed periodically to npm under the @types/ prefix as a convention. If you can't find the type declarations for a particular library use the TypeSearch web app to find it (You can try stampit from the stamps chapter section for instance).

Concluding

And that is TypeScript! This was the longest chapter in the book but I hope that it was entertaining and interesting enough to carry you to the end. Let's make a quick recap so you get a quick reminder that'll help you remember all the TypeScript awesomeness you've just learned.

TypeScript is a superset of JavaScript that includes a lot of ESnext features and type annotations. By far, the defining feature of TypeScript are its use of types. Type annotations allow you to provide additional metadata about your code that can be used by the TypeScript compiler to provide a better developer experience for you and your team at the expense of code verbosity.

TypeScript is a superset of ES6 and expands on its features with a lot of ESnext improvements and TypeScript specific features. We saw several ESnext features like class members and the new Objects spread and rest operators. We also discovered how TypeScript enhances classes with parameter properties and property accessors, and brings a new Enum type that allows you to write more intentional code.

Type Annotations are TypeScript's bread and butter. TypeScript extends JavaScript with new syntax and semantics that allow you to provide rich information about your application types. In addition to being able to express primitive types, TypeScript introduces interfaces, generics, intersection and union types, aliases, type guards, etc... All of these mechanisms allow you to do a new type of meta-programming that lets you improve your development experience via type annotations. Still adding type annotations can be a little daunting and a lot of work, in order to minimize this, TypeScript attempts to infer as much typing as it can from your code.

In the spirit of JavaScript and duck-typing, TypeScript has a structural typing system. This means that types will be equivalent if they share the same structure, that is, if they have the same properties. This is opposed to nominal typing systems like the ones used within C# or Java where type equivalence is determined by explicitly implementing types. Structural typing is great because it gives you a lot of flexibility and, at the same time, great compile-time error detection and improved tooling.

In the front-end development world we're seeing an increased adoption of TypeScript, particularly, as it has become the core language for development in Angular. Moreover, it is also available in most of the common front-end frameworks, IDEs, tex-editors and front-end build tools. It is also well supported in third-party libraries through type definitions and the DefinitelyTyped project, and installing type definitions for a library is as easy as doing an npm install.

From a personal perspective, one of the things I enjoyed the most about JavaScript coming from the world of C# was its terseness and the lack of ceremony and unnecessary artifacts. All of the sudden, I didn't need to write PurchaseOrder purchaseOrder or Employee employee any more, an employee was an employee, period. I didn't need to write a seemingly infinite amount of boilerplate code to make my application flexible and extensible, or fight with the language to bend it to my will, things just worked. As I saw the release of TypeScript I worried about JavaScript losing its soul and becoming a language as rigid as C# or Java. After experiencing TypeScript developing Angular applications, its optional typing, the great developer experience and, above all, the fact that it has structural typing I am hopeful. It'll be interesting to follow its development in the upcoming months and years. It may well be that all of us will end up writing TypeScript for a living.

mooleen.says('You shall only use types!?...');

bandalf.says("I've got my magic back... " + 
  "but for some reason it won't... work");

mooleen.says("I, too, can feel the bond with the " +
  "currents of magic again");

randalf.says("The Order of the Red Moon...");

red.says("There are our weapons! Under the obelisk!");

/*
The group makes a precarious circle beside the obelisk as
the hordes of lizard-like beast surround them.
*/

randalf.says("types... Yes! " + 
  "Now I remember, The Last Stand and the Sacred Order. " +
  "Their story lies between history and legend. " +
  "It is said that they cultivated an obscure " +
  "flavor of JavaScriptmancy. The legends say that " +
  "they expanded it and enriched it with types...");

bandalf.says("Excellent. And what does that mean?");

rat.says("It means we're dead");
red.says("A glorious death!");

randalf.says("Well they were a very guarded Order " +
  "and they were exterminated to the last woman " + 
  "in The Last Stand or so the story says..." + 
  "In the deep jungles of Azons.");

mooleen.whispers("Azons...");

/* 
  The sisters surround her on the battlements,
  all wearing the black of the order in full armor.
  The fort has an excellent view of the thick,
  beautiful jungle below and of the unending hosts
  of lizardmen surrounding them.
  The Grand Commander shouts: 'To Arms sisters!'
  'For one last time!'
*/

mooleen.says("Types... Types... Types!");
mooleen.says("I remember...");

Exercises

Experiment JavaScriptmancer!

You can experiment with these exercises and some possible solutions downloading the source code from GitHub.

Earn Some Time! A wall of ice!

The beasts are quickly approaching, gain some breathing room by erecting an ice wall between them and the group. The wall should be at least 100 feet high, 7 feet deep and 700 feet long to be able to surround the group.

The Wall should satisfy the following snippet:

const iceWall = new Wall(MagicElement.Ice, {
                          height: 100,
                          depth: 7, 
                          length: 700});

console.log(iceWall.toString());
// => A wall of frozen ice. It appears to be about 100 feet high
//    and extends for what looks like 700 feet.

iceWall.element = MagicElement.Fire;
// => [ts] Cannot assign to 'element' because it is 
//         a constant or a read-only property.
iceWall.wallOptions.height = 100;
// => [ts] Cannot assign to 'height' because it is 
//         a constant or a read-only property.

Hint: You can use an enum to represent the MagicElement, an interface to represent the WallSpecifications and a class for the Wall itself. Remember to add type annotations!

Solution


enum MagicElement {
  Fire = "fire",
  Water = "water",
  Earth = "earth",
  Air = "windy air",
  Stone = "hard stone",
  Ice = "frozen ice"
}

interface WallSpecs{
  readonly height: number,
  readonly depth: number,
  readonly length: number
}

class Wall {
  constructor(readonly element: MagicElement, 
              readonly specs: WallSpecs){ }

  toString(){
    return `A wall of ${this.element}. It appears to be about ` +
      `${this.specs.height} feet high and extends for what ` +
      `looks like ${this.specs.length} feet.`;
  }
}

const iceWall = new Wall(MagicElement.Ice, {
                          height: 100,
                          depth: 7, 
                          length: 700});

console.log(iceWall.toString());
// => A wall of frozen ice. It appears to be about 100 feet high
//    and extends for what looks like 700 feet long.

// iceWall.element = MagicElement.Fire;
// => [ts] Cannot assign to 'element' because it is 
//         a constant or a read-only property.
// iceWall.wallOptions.height = 100;
// => [ts] Cannot assign to 'height' because it is 
//         a constant or a read-only property.

world.randalf.gapes()
// => Randalf gapes

world.randalf.says('How?');
world.mooleen.says('I just remembered...');

world.randalf.says('Remember?');
world.randalf.says("You look very young for being millennia old");

world.mooleen.shrugs();
// => Moleen shrugs
world.mooleen.says("Brace yourselves... they're coming " + 
  "beware if they open their jaws and seem to catch breath " +
  "they breathe fire");

Freeze The Lizards!

You've earned some time. Now you can take this breather to observe the lizards, model them appropriately and craft a frost spell that will send them to the lizard frozen hell.

This is what you can observe:

giantLizard.jumps();
// => The giant lizard gathers strength in its 
//    4 limbs and takes a leap through the air
giantLizard.attacks(red);
// => The giant lizard attacks Red with great fury
giantLizard.breathesFire(red);
// => The giant lizard opens his jaws unnaturally wide
//    takes a breath and breathes a torrent of flames
//    towards Red
giantLizard.takeDamage(Damage.Physical, 20);
// => The giant lizard has extremely hard scales
//    that protect it from physical attacks (Damage 50%)
//    You damage the giant lizard (-10hp)
giantLizard.takeDamage(Damage.Cold, 20);
// => The giant lizard is very sensitive to cold.
//    It wails and screams. (Damage 200%)
//    You damage the giant lizard (-40hp)

Create a frost spell that fulfills this snippet:

frost(giantLizard, /* mana */ 10);
// => The air surrounding the target starts quickly forming a
//    frozen halo as the water particles start congealing.
//    All of the sudden it explodes into freezing ice crystals
//    around the giant lizard.
//    The giant lizard is very sensitive to cold.
//    It wails and screams. (Damage 200%)
//    You damage the giant lizard (-2000hp)

Hint: Create a interface using the observations above and use that new type in your frost function. Reflect about the required contract to cause damage on an enemy.

Solution


enum DamageType {
  Physical,
  Ice,
  Fire,
  Poison
}

// We only need an interface that
// describes something that can be damaged
interface Damageable{
  takeDamage(damageType: DamageType, damage: number);
}

function frost(target: Damageable, mana: number){
  // from the example looks like damage 
  // can be calculated based on mana
  const damage = mana * 100;
  console.log(
    `The air surrounding the target starts quickly forming a ` + 
    `frozen halo as the water particles start congealing. ` +
    `All of the sudden it explodes into freezing ice crystals ` +
    `around the ${target.toString()}.`);
  target.takeDamage(DamageType.Ice, damage);
}

console.log('A giant lizard leaps inside the wall!');
// this method returns a Lizard object (see samples)
const giantLizard = world.getLizard();

world.mooleen.says('And that is as far as you go');

frost(giantLizard, /* mana */ 2);
// => The air surrounding the target starts quickly forming a 
//    frozen halo as the water particles start congealing. 
//    All of the sudden it explodes into freezing ice crystals 
//    around the giant lizard.
//    The giant lizard is very sensitive to cold.
//    It wails and screams. (Damage 200%)
//    You damage the giant lizard (-400hp)
//    The giant lizard dies.

world.mooleen.laughsWithGlee();
// => Mooleen laughs with Glee

/* 
More and more lizards make it into the fortified area.
Mooleen, Red, randalf and bandalf form a semicircle against
the obsidian obelisk and fight fiercely for every inch.
When the lizards are about to overwhelm the group a huge furry
figure flashes in front of them charging through the lizard 
front line and causing enough damage to let the company regroup.
*/

world.mooleen.says('What?');
world.rat.says('Happy to serve!');

world.mooleen.says('You can do that?!');
world.rat.says('Err... we familiars are very flexible creatures');

world.mooleen.says("Why didn't you say it before?");
world.rat.says("Oh... the transformation is incredibly painful");
world.rat.says("And I bet you'd want to ride on my back." + 
    "I'm not putting up with that");

Wholesale Destruction!

Killing the beasts one by one won't cut it. We need a more powerful spell that can annihilate them in groups. Design an iceCone spell that can impact several targets at once.

It should fulfill the following snippet of code:

iceCone(lizard, smallerLizard, greaterLizard);
// => Cold ice crystals explode from the palm of your hand
//    and impact the lizard, smallerLizard, greaterLizard.
//    The lizard is very sensitive to cold.
//    It wails and screams. (Damage 200%)
//    You damage the giant lizard (-500hp)
//    The smaller lizard is very sensitive to cold.
//    It wails and screams. (Damage 200%)
//    You damage the giant lizard (-500hp)
//    etc...

Hint: you can use rest parameters and array type annotations!

Solution

function iceCone(...targets: Damageable[]){
  const damage = 500;
  console.log(`
Cold ice crystals explode from the palm of your hand
and impact the ${targets.join(', ')}.`);
  for(let target of targets) {
    target.takeDamage(DamageType.Ice, damage);
  }
}

iceCone(getLizard(), getLizard(), getLizard());
// => Cold ice crystals explode from the palm of your hand
// and impact the giant lizard, giant lizard, giant lizard.
// The giant lizard is very sensitive to cold.
// It wails and screams. (Damage 200%)
// You damage the giant lizard (-1000hp)
// The giant lizard dies.
// etc...

world.mooleen.says('Yes!');

/* 
Mooleen looks around. She's fending off the lizards fine but
her companions are having some problems. 

Red is deadly with the lance and shield but his lance, 
in spite of of his massive strength, hardly penetrates
the lizards' thick skin. 

Bandalf is slowly catching up and crafting ice spells 
and Randalf, though, a master with the quarterstaff can
barely fend off the attacks from a extremely huge lizard.

Things start to look grimmer and grimmer as more lizards jump over
the wall around the obelisk.
*/

world.mooleen.says('I need to do something quick');

Empower Your Companions with Enchantments!

Things are looking grim. Your only chance is to empower your companions so that you can offer a strong united front against the growing host of enemies. Craft an enchant spell that can enchant weapons and armor with elemental properties.

The enchant spell should satisfy the following snippet of code:

quarterstaff.stats();
// => Name: Crimson Quarterstaff
// => Damage Type: Physical
// => Damage: d20
// => Bonus: +20
// => Description: A quarterstaff of pure red

enchant(quarterstaff, MagicElement.Ice);
// => You enchant the Crimson Quarterstaff with a frozen 
//    ice incantation
//    The weapon gains Ice damage and +20 bonus damage

quarterstaff.stats();
// => Name: Crimson Quarterstaff
// => Damage Type: Ice
// => Damage: d20
// => Bonus: +40

cloak.stats();
// => Name: Crimson Cloak
// => Type: cloak
// => Protection: 20
// => ElementalProtection: none
// => Description: A cloak of pure red

enchant(cloak, MagicElement.Fire);
// => You enchant the Crimson Cloak with a fire incantation 
//    The Crimson Cloak gains +20 fire protection

cloak.stats();
// => Name: Crimson Cloak
// => Type: cloak
// => Protection: 20
// => ElementalProtection: Fire (+20)
// => Description: A cloak of pure red

Hint: Use union types and type guards within the enchant spell to allow it to enchant both Weapon and Armor

Solution


class Weapon {
  constructor(public name: string,
              public damageType: DamageType,
              public damage: number,
              public bonusDamage: number,
              public description: string){}
  stats(){
    return `
Name: ${this.name}
Damage Type: ${this.damageType}
Damage: d${this.damage}
Bonus: +${this.bonusDamage}
Description: ${this.description}
      `;
  }

  toString() { return this.name; }
}

enum ArmorType {
  Cloak = 'cloak',
  Platemail = 'plate mail'
}

interface ElementalProtection {
  damageType: DamageType;
  protection: number;
}

class Armor {
  elementalProtection: ElementalProtection[] = [];
  constructor(public name: string,
              public type: ArmorType,
              public protection: number,
              public description: string){}
  stats(){
    return `
Name: ${this.name}
Type: ${this.type}
Protection: ${this.protection}
ElementalProtection: ${this.elementalProtection.join(', ') || 'none'}
Description: ${this.description}
      `;
  }
  toString() { return this.name; }
}

function enchant(item: Weapon | Armor, element: MagicElement){
  console.log(`You enchant the ${item} with a ${element} incantation`);
  if (item instanceof Weapon){
    enchantWeapon(item, element);
  } else{
    enchantArmor(item, element);
  }

  function enchantWeapon(weapon: Weapon, element: MagicElement){
    const bonusDamage = 20;
    weapon.damageType = mapMagicElementToDamage(element);
    weapon.bonusDamage += bonusDamage;
    console.log(`The ${item} gains ${bonusDamage} ` + 
                `${weapon.damageType} damage`);
  }
  function enchantArmor(armor: Armor, element: MagicElement){
    const elementalProtection = {
      damageType: mapMagicElementToDamage(element),
      protection: 20,
      toString(){ return `${this.damageType} (+${this.protection})`}
    };
    armor.elementalProtection.push(elementalProtection);
    console.log(`the ${item} gains ${elementalProtection.protection}` + 
                ` ${elementalProtection.damageType} incantation`);
  }
}

function mapMagicElementToDamage(element: MagicElement){
  switch(element){
    case MagicElement.Ice: return DamageType.Ice;
    case MagicElement.Fire: return DamageType.Fire;
    default: return DamageType.Physical;
  }
}

let quarterstaff = getQuarterstaff();
console.log(quarterstaff.stats());
// => Name: Crimson Quarterstaff
//    Damage Type: Physical
//    Damage: d20
//    Bonus: +20
//    Description: A quarterstaff of pure red

enchant(quarterstaff, MagicElement.Ice);
// => You enchant the Crimson Quarterstaff with a frozen ice incantation
//    The Crimson Quarterstaff gains 20 Ice damage

console.log(quarterstaff.stats());
// Name: Crimson Quarterstaff
// Damage Type: Ice
// Damage: d20
// Bonus: +40
// Description: A quarterstaff of pure red

let cloak = getCloak();
console.log(cloak.stats());
// Name: Crimson Cloak
// Type: cloak
// Protection: 20
// ElementalProtection: none
// Description: A cloak of pure red

enchant(cloak, MagicElement.Fire);
// You enchant the Crimson Cloak with a fire incantation
// the Crimson Cloak gains 20 Fire incantation

console.log(cloak.stats());
// Name: Crimson Cloak
// Type: cloak
// Protection: 20
// ElementalProtection: Fire (+20)
// Description: A cloak of pure red

world.mooleen.says('Awesome! This will do!');

/*

As soon as Mooleen enchants the group's weapons and
armor the battle takes a different turn. Where previously
a lizard would've remained impassible after receiving a wound
now there's wails and shouts of beast pain surrounding 
the group...

*/

world.mooleen.says('haha! To Arms Sisters!');
world.red.says('What?');

  1. The editor that you use should have a good integration with the TypeScript compiler to provide this type of service. Many of the most common IDEs and text editors have that support. 

  2. Like params in C#. 

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

  4. This command uses the TypeScript React Started in the background http://bit.ly/ts-react-starter 

Top comments (27)

Collapse
 
radicalbee profile image
Abdu

It seems "TypeScript" boom has started. I think it would have been great if the article was split in parts.

Collapse
 
aviaryan profile image
Avi Aryan

+1, but huge kudos to Jaime for putting it out. I will go through it while learning TypeScript.

Collapse
 
vintharas profile image
Jaime González García

Thank you Avi! :D

Collapse
 
vintharas profile image
Jaime González García • Edited

Thanks for your comment! :D It certainly feels that way. Thanks for your feedback! :)

Collapse
 
radicalbee profile image
Abdu

Cheers!

Collapse
 
simov profile image
simo

First of all thumbs up for mentioning the disadvantages as well. I think we should all try to be a bit more objective in our opinions than just merely following trends.

Well basically all the points you mentioned as advantages doesn't sound like they are worth the trade-off. But that's just me. I can agree only with the:

Great for large-scale applications and teams.

But do we all really work in such conditions? I highly doubt so.

The disadvantages on the other hand surely look like minor things:

  • TypeScript requires a transpilation step
  • You need to learn type annotations syntax and related artifacts
  • It is verbose
  • It sometimes falls out of line with the ECMAScript standard

But that's what hampers your productivity and slows you down.

The whole point of the dynamic languages to exist is to allow faster prototyping.

And the first thing we think about prototyping is the initial phase of a project.

But (micro) prototyping happens all the time:

The act of human being sitting behind a keyboard and writing commands to a computer to execute is an act of prototyping.


Anyway, apart from probably being a good fit for really large projects, TypeScript also helps to alleviate the fact that the majority of the web developers had started their careers using dynamically typed languages, and know nothing about types.

Types are always there, but the dynamic languages, like JavaScript, require a bit more diligence on the developer side, and a prior knowledge of typed languages.

Collapse
 
qm3ster profile image
Mihail Malo

In all seriousness, we need to organize a special task force to lynch people who complain that "it's not just JavaScript". They are the reason nice syntax like newtype constructors and methods on these primitive newtypes isn't getting added.
Why do the people actually using TS every day have to suffer just because of criticism of those who haven't been onboarded yet, and maybe never will?

Collapse
 
vintharas profile image
Jaime González García

No lynching! Just hugs!

I think that anything on the side of type annotations is fair game.

Collapse
 
qm3ster profile image
Mihail Malo

Hug them until they realize the error of their ways!

Collapse
 
simov profile image
simo • Edited

This Cost vs Benefit Analysis of TypeScript recently came out. You might want to check it out.

Collapse
 
vintharas profile image
Jaime González García

Awesome! Thanks for sharing! I really enjoy reading Eric Elliot 😀

Collapse
 
qm3ster profile image
Mihail Malo

Oh carp. I linked to it as well before seeing this.

Collapse
 
qm3ster profile image
Mihail Malo

What do you think about the alleged "Cost of TypeScript"?

Personally, I think it's definitely a velocity improvement, both in legacy code and greenfield, because the main benefit is developer tooling and not release testing, unlike what that article focuses on.

Do you think this article is just capitalizing on the TypeScript hype?

Collapse
 
vintharas profile image
Jaime González García

I think it's great that people have different opinions and points of views. We're all the richer for it :D You need sceptics to keep us all honest and continue improving things.

I read the article a while back but I remember it had a couple of things that bugged me:

  1. It seems to present a false dichotomy which is TypeScript vs Code Reviews/TDD, etc. As if adopting TypeScript means letting go of great development practices like code reviews, TDD, automated testing, etc. TypeScript is just one tool more that makes you more productive in addition to all your current arsenal of tools and practices.
  2. It seems to equate the ease of use of TypeScript with other type inference tools like tern-js that are much harder to setup. TypeScript works out of the box. tern-js doesn't.

But as said, I read it a while back and I may have misunderstood him. I think that regardless of the costs, all in all, he seemed to have a good opinion of TypeScript. I think that a lot of the pain points that Eric is experiencing with TypeScript stem from the fact that he uses a very FP-like way of programming in JavaScript and TypeScript isn't as feature complete for specifying the types of FP constructs he favours like higher order functions, etc.

Collapse
 
jonrandy profile image
Jon Randy 🎖️ • Edited

TypeScript seems like mollycoddling for developers who are unwilling to work / are incapable of working with JS as it is, and don't want to change their habits coming from a strongly typed language - at the expense of more complex tooling, and a runtime overhead

Collapse
 
chuckjonas profile image
Charlie Jonas

I see this exact same comment on pretty much every article written about typescript. It outs you as someone who doesn't fully understand what typescript is or how it works (or at the very least hasn't had a chance to work with it for a long period of time).

This statement is almost like saying, "RUST seems like mollycoddling for developers who are unwilling to work / are incapable of working with C". Nope, RUST is simply just a better, safer alternative for system level programming.

Typescript is simply just an better alternative to javascript for web and node applications ( "applications" is the keyword here). Should you take time to migrate your small local business websites to typescript. Probably not.

Points:

1: The tooling complexity is a 1 time cost. A lot of times you already have bundling or babel setup and it's as simple as adding 1 additional step.
2: You can still write the EXACT same javascript that you were writing before, but now with the options to have types. The transpired code will be pretty much the exact same as what you would have had without types.

3: The dev productivity and code resiliency gains that you get from having a fully typed code base are immeasurable.
4: The additional code features that it does add, are added with the sole intention of fixing things that javascript has messed up. The typescript team has expressed that it is a priority to keep typescript as close to js as possible.

I've yet to find someone who has really made the transition and regrets it (I'm sure they are out there but they must be a minority). In fact, once you pick-up typescript you will DREAD the thought of working without it.

There's a reason it's growing so fast. My most sincere advise would be to not be one of those guys who resists change and gets left behind because they are simply stuck in their old ways!

Collapse
 
remojansen profile image
Remo H. Jansen • Edited

The compiler removes all types during the compilation process, which means that there is no possibility of runtime overhead. Maybe you are referring to some polyfills for things such as async/await when you target ES5.

These polyfills are required if you don't want to work with callback hells. A polyfill adds some runtime overhead but it is not a TypeScript weak point: you will have the exact same problem if you use JS with Babel.

Also, assuming that TypeScript fans are "unwilling to work / are incapable of working with JS" seems completely bias to me.

After working many years with TypeScript I can say without doubts that daily work tasks such as refactoring can take a significantly smaller amount of work thanks to the help of the TypeScript type system.

Collapse
 
wolfhoundjesse profile image
Jesse M. Holmes

I disagree, but I would as a lover of TypeScript. The last language I preferred wasn’t strongly typed, and I find it to be a relief.

The tooling isn’t all that complex—today you can throw Parcel into a project and mostly forget about your TypeScript configuration, and my editor is built in the same language, so the tools work very well together.

I’m not sure what you mean by runtime overhead. It’s JavaScript by then, and Interfaces—the biggest win for the work I’m doing—don’t even compile.

Collapse
 
colinbradley profile image
Colin Bradley

TS is designed for large projects with lots of people.
JS is just fine in small projects, with people that know what they are doing and communicate well.
I wouldn't dismiss strong typing as just a habit, it does have some benefits.
Also, there is no runtime overhead - TS compiles to JS, readable JS at that.

Collapse
 
vitalcog profile image
Chad Windham

I work on a big team writing typescript every day. The more I use typescript the more I dislike it. Yes, there are all the "super cool reasons" people have about typescript "being safer" (it's not) or speeding up project development by shortening the debugging process (it doesn't). It is very, very hard for me to see any true value to writing a bloated version of a language that trans-piles to the same language it bloated in the first place. My experience, lots of claims about typescript that look good on paper, don't really pan out in real world development (as in, if it does help in the ways that it claims, the amount it improves is so minimal that I can't perceive it). I've said it before and I'll say it again, typescript is made for developers that hate javascript and want to not write javascript, simple as that.

Just to be clear, I personally hate typescript, but to anybody out there who loves it, relax, its cool. I'm glad you like it. And as a stand alone language, I would love writing typescript, I just hate writing a bloated superset of javascript that is trans-piled back to javascript. I think the maker of Node.js is actually about to release a new project, basically Node but with typescript (not using typescript to write javascript, but actually server side code that is just written in typescript). And if/when that happens, I'll fully support it and hope it takes off. But until then, I'm hoping that Hypescript stops being cool...

Thread Thread
 
qm3ster profile image
Mihail Malo

By "release", do you mean Deno reaching v1.0?

Thread Thread
 
vitalcog profile image
Chad Windham

Yeah good point. I suppose that is pretty much what I meant. In all honesty it isn't something I've been keeping up with strongly, last I heard about it, it was still in the planning to do this but haven't started phase (so quite a while...) so thanks for sharing that link! ;)

Collapse
 
jonrandy profile image
Jon Randy 🎖️ • Edited

The last time I looked at the resulting compiled JS, it was readable, sure, but the amount of superfluous 'belt and braces' code that ended up in there messed with my head. JS written by hand would have done the same job more efficiently - minus all the tooling, transpiling etc.

It just seems a waste of time to me. Each to their own I guess

Thread Thread
 
qm3ster profile image
Mihail Malo

Really? That sounds shocking to me.

Can you please post some of the

superfluous 'belt and braces' code

?
The only thing I ever found superfluous in TS output was the import/export syntax when converting to commonjs modules.

Collapse
 
caseycole589 profile image
Casey Cole

Totally agree I have to do c# back end and it takes so much longer and is so much more complex while trying to defend having the front end converted to typescript.

Collapse
 
vintharas profile image
Jaime González García • Edited

Hi Jon! Thanks for your comment! :D hehe Did you know that the whole point of the JavaScriptmancy series was to teach C# developers that vanilla JavaScript was pretty awesome (when you actually got to learn it)? xDD

I remember that when TypeScript came out (I think it was around 2014 for the 1.0 release) I was quite sceptical about the whole thing. I went from C# to JavaScript and even though I loved C#, I really enjoyed the freedom from types in JavaScript. Particularly when doing object composition patterns that required a lot of boilerplate in C#. So I was worried that TypeScript would be just C#-ifying your JavaScript :D.

Anyhow, I think that the project has improved a lot since the early beginnings and I celebrate their success and the new ideas and great developer experience they've brought to JavaScript.

One cool thing about web development is that it is the meeting place for a lot of different communities of developers that wouldn't interact (so much) otherwise and, as such, it becomes a great place for cross pollination and sharing ideas and things that have worked great for these particular communities. One of those examples was CoffeeScript, and another one is TypeScript.

I see TypeScript as, "yo! We've been using static typing to provide great compile time checks and awesome developer productivity for years. Perhaps we can bring that same experience to the web". This developer experience naturally feels more comfortable to developers with strongly typed backgrounds since they get to use a lot of the things they're accustomed to.

A great thing for the rest of the JavaScript developer community is that TypeScript is very respectful of the nature of JavaScript. A good example of this is that TypeScript has structural typing (that is duck typing) as opposed to a nominal type system like Java or C#. The types of things are defined by their structure rather than by a name.

In regards to more complex tooling, I think that for most projects there's not gonna be a big difference in tooling since either you're gonna have TypeScript or Babel. Since types disappear after transpilation and you're left with JavaScript, I'm not sure there's a big runtime overhead. The polyfilling generated by TypeScript is negligible and varies based on the target you set in your compiler configuration.

Collapse
 
revskill10 profile image
Truong Hoang Dung

Introducing TS into your codebase is useful in two cases to me:

  • For a large project with many developers working on it.
  • For a small library, which has a nearly stable API.