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
/*
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');
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!
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.
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;
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);
}
So we go and write the following:
manaReserves += rechargeMana({
name: 'light potion of mana',
manaModifier: 1.5
});
When we execute the piece of code above, it explodes with the following error:
// => Uncaught TypeError: Math.rand is not a function
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'.
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
})
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'.
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);
}
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:
rechargeMana
doesnât have the expected interface. That is, when it lacks the manaModifier
property which is necessary for the function to work.potion
object within the body of the function.potion
inside the rechargeMana
function which is a great developer experience[statementcompletion]. 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.[statementcompletion]: 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.
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
};
At some point in our program we could end up calling this code by mistake:
rechargeMana(potionOfStrength);
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; }'.
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.
By enhancing your JavaScript with new features, type annotations and static typing TypeScript provides these advantages:
async/await
.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.On the minus side:
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.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
You can verify that the installation has worked correctly by running:
$ tsc -v
Version 2.4.2
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
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
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!`);
}
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'.
Letâs fix it by saluting yourself. Update the code above to include your name inside a string:
sayHello('<Your name here>');
Now you can compile the TypeScript file using the compiler within the terminal (Visual Studio comes with an embedded terminal that you can run inside the editor which is very convenient). Type:
$ tsc hello-world.ts
This will tell the TypeScript compiler to transpile your TypeScript application into JavaScript that can run in the browser. It will result in a vanilla JavaScript file hello-world.js
that contains the following code:
function sayHello(who) {
console.log("Hello " + who + "! I salute you JavaScript-mancer!");
}
sayHello('<Your name here>');
Beautiful vanilla JavaScript as if you had typed it with your bare hands. You can use node
to run this file:
$ node hello-world.js
Hello <Your name here>! I salute you JavaScript-mancer!
And TaDa! Youâve written, transpiled and run your first TypeScript program! World here we come!
Since it can be slightly tedious to run the TypeScript compiler every time you make changes in your ts
files, you can setup the compiler in watch mode. This will tell the TypeScript compiler to monitor your source code files and transpile them whenever it detects changes. To setup the TypeScript compiler in watch mode just type the following:
$ tsc -w hello-world.ts
10:55:11 AM - Compilation complete. Watching for file changes.
In the upcoming sections we will discover some of the great features you can use within TypeScript, all you need to know about TypeScript type annotations and what you need to think about when using TypeScript in real-world projects.
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.
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`
}
}
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`
}
}
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`
}
}
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:
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`
}
}
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.
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;
}());
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);
}
};
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);
}
};
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'.
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.
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:
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â.--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.
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)`);
}
};
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'
};
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)`);
}
};
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
}
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);
}
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 thetype
provided for the spell is of typeSchoolsOfMagic
, and not only that, when using an editor such as Visual Studio Code it will give us all the available options (Fire
,Water
,Air
andEarth
) 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)`);
}
}
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'.
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 = {}));
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"
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
}
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
}
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.
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";
Again we define an interface in terms of this new type:
interface Spell {
type: SchoolsOfMagic,
damage: number,
cast(target: any);
}
And weâre ready to create spells. Using anything other than the allowed strings will result in a transpilation error:
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'.
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 [multiple]. Instead of using the arguments
object like we used to do prior to ES6:
[multiple]: Like params
in C#.
function obliterate(){
// Unfortunately arguments is not an array :O
// so we need to convert it ourselves
var victims = Array.prototype.slice.call(arguments,
/* startFromIndex */ 0);
victims.forEach(function(victim){
console.log(victim + " wiped off of the face of the earth");
});
console.log('*Everything* has been obliterated, ' +
'oh great master of evil and deceit!');
}
We can use rest syntax to collect all incoming arguments directly into an array victims
:
function obliterate(...victims){
victims.forEach(function(victim){
console.log(`${victim} wiped out of the face of the earth`);
});
console.log('*Everything* has been obliterated, ' +
'oh great master of evil and deceit!');
}
On the other hand the spread operator works sort of in an opposite way to rest parameters. Instead of taking a variable number of arguments and packing them into an array, the spread operator takes an array and expands it into its compounding items.
Following this principle the spread operator has many use cases[use-cases].Like concatenating arrays:
[use-cases]: Go back and review JavaScript-mancy: Getting Started for lots more of use cases!
let knownFoesLevel1 = ['rat', 'rabbit']
let newFoes = ['globin', 'ghoul'];
let knownFoesLevel2 = [...knownFoesLevel1, ...newFoes];
Or cloning them:
let foes = ['globin', 'ghoul'];
let clonedFoes = [...foes];
Object Spread and Rest brings this same type of functionality that is available in arrays to objects.
A great use case for the Object spread operator are mixins. In previous 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;
}
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
};
}
The object spread operator essentially says: get all properties of <em>wizard</em>
, <em>canBeIdentifiedByName</em>
and <em>canCastSpells</em>
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']
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'}
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 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'.
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'.
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'.
and even objects:
function frost(minion: {hitPoints: number}) {
const damage = 10;
console.log(`${minion} is covered in frozy icicles (- ${damage} hp)`);
minion.hitPoints -= damage;
}
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)
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; }'.
An even better way to annotate objects is through interfaces.
Interfaces are reusable and less verbose than straight object type annotations. A Minion
interface could be described as follows:
interface Minion {
hitPoints: number;
}
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;
}
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, interfacesare 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;
}
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)
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)
And so does a simple object literal with the hitPoints
property:
frosty({hitPoints: 100});
// => [object Object] is covered in frozy icicles (-10hp)
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'.
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)
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 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);
}
}
Which you can verify using this snippet of code:
let wizard: Wizard = bard;
let anotherWizard: Wizard = new MagicCreature(120);
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'.
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)
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)
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
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
And TypeScript will know that the function double
returns a number.
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) {}
}
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;
}
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)
Normally we would have the class
implement the desired interface
. For instance:
class ClassyMinion implements Minion {
constructor(public hitPoints: number) {}
}
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;
}());
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:
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:
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.interfaces
are a lightweight way to provide type information to data that may be foreign to your application like data coming from web servicesSo, 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.
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:
Letâs take a look at each of them, why they are needed and how to use them.
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;
// etc
}
interface StringArray {
push(s: string);
pop(): string;
// etc
}
// etc...
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;
// etc
}
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...
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;
}
}
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;
}
And now this will be a perfectly usable box:
let box: ConstrainedCell<MagicCreature>;
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; }'.
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
};
}
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);
}
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
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'.
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);
};
}
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
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);
};
}
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'.
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
.
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
You can create an alias Wizard
and use that instead:
type Wizard = WizardProps & NameMixin & SpellMixin;
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
};
}
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.
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
Likewise using React and the Create React App tool (also known as CRA) starting a React project with TypeScript takes only typing[react-starter]:
$ create-react-app my-new-app --scripts-version=react-scripts-ts
[react-starter]: This command uses the TypeScript React Started in the background http://bit.ly/ts-react-starter
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.
tsconfig.json
FileThe 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:
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"
]
}
For a full reference of all the available options take a look at the TypeScript documentation.
The TypeScript compiler
tsc
has two different modes of operation: with or without input files. > When you donât specify input files while executingtsc
the TypeScript compiler will try to find an availabletsconfig.json
file with its configuration. > When you do specify input files the TypeScript compiler will ignoretsconfig.json
. That is why in previous sections we didnât need to define atsconfig.json
file when we runtsc 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
And you can install the type declarations for that library using @types/<name-of-library-in-npm>
:
$ npm install --save-dev @types/ramda
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)}`);
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 TypeSearchweb app to find it (You can try stampit
from the stamps chapter section for instance).
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...");
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 least100
feet high,7
feet deep and700
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 theWallSpecifications
and a class for theWall
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");
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");
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');
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.> Theenchant
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 bothWeapon
andArmor
### 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?');
#javascript