Functions in TypeScript work in exactly the same way as functions in ES2015, so we can write functions using the fat arrow syntax as well as leverage default parameters or even the spread operator.
But let's not forget that TypeScript is a superset of JavaScript and it actually extends ES2015 with support for data types as well as Interfaces.
We can combine these features of the language to get some great functionality for us that will allow for writing awesome code and leveraging the powers of our IDE.
Remember, we can not only define the argument types for our function but we can also specify what datatype the function should return – and if we are not returning anything we can also specify the void
keyword as a return type:
function add(n: number, m: number): number {
return n + m;
}
function fight(weapon: string): void {
// no return
console.log(`Warrior fights with ${weapon}`);
}
We can also use types on rest parameters – this is ideal for checking types, especially if our function achieves a numerical operation – in which case we want to make sure that the arguments that we pass in are all numbers:
function add(...numbers: number[]): number {
return numbers.reduce((acc, number) => acc + number);
}
add(1, 2);
add('1', 2); // Argument of type '"1"' is not assignable to parameter of type 'number'
So far so good, but now let's add interfaces and arrow functions into the mix as well and unleash the true power of TypeScript:
interface IWarrior {
name: string;
weapon: string[];
health: number;
pickWeapon(this: IWarrior): () => Weapon;
}
interface Weapon {
name: string;
hitPower: number;
}
const warrior: IWarrior = {
name: 'Dave the Nomad',
weapon: ['sword', 'knife', 'epic axe', 'spear'],
health: 100,
// NOTE: The function now explicitly specifies that its callee must be of type Deck
pickWeapon: function (this: IWarrior) {
return () => {
const warriorWeapon = Math.floor(Math.random() * this.weapon.length);
const weaponHitPower = Math.floor(Math.random() * 100);
return { name: this.weapon[warriorWeapon], hitPower: weaponHitPower };
};
},
};
const myWarrior = warrior;
const weaponChoice = myWarrior.pickWeapon();
const weapon = weaponChoice();
console.log(`My warrior is ${myWarrior.name}. (Health: ${myWarrior.health})`);
console.log(
`Randomly selected weapon: ${weapon.name} with hit power ${weapon.hitPower}.`
);
What are we doing here? Imagine that you are working on a game where you can create a warrior and randomly select a weapon with a hit power for each warrior that you create.
First things first, we create two interfaces, one for the warrior itself and another one for the weapon. Notice how the pickWeapon()
method passes in this as a parameter with type IWarrior
. This will mean that ‘
this
is of type IWarrior
now as opposed to any
, therefore it will force us using the right datatype.
Also notice that the pickWeapon()
method defined in the IWarrior
interface requires us to return something that matches our other interface: Weapon
.
We then go ahead and create our warrior object where we assign our warrior a name
, a health
and an array of weapons
. To fulfil the contract specified by the IWarrior
interface we need to make sure that we add a pickWeapon()
method too.
In this method we are randomly going to select an index based on the length of the weapon array belonging to the warrior object as well as a random value for the weapon's hit power and assign those to two variables. Finally we are returning an object that contains the weapons's name and a hitpower property (therefore fulfilling the IWarrior
interface contract).
There are a bunch of things to notice here: the obvious is that we are using interfaces and datatypes throughout the code. Also notice, by using the fat arrow syntax (that we can also use in ES2015) we can safely use the this
keyword in the context of another function since this time this
(excuse the pun) is fetched lexically and there's no need to call .bind()
or .apply()
nor there's a need to assing the value of this
to another variable early on within the object.