May 07, 2021 TypeScript
Functions are the foundation of JavaScript applications. I t helps you implement abstraction layers, simulation classes, information hiding, and modules. I n TypeScript, functions are still the primary place to define behavior, although classes, namespaces, and modules are already supported. TypeScript adds additional functionality to the JavaScript function, making it easier for us to use.
Like JavaScript, TypeScript functions can create functions with names and anonymous functions. You're free to choose the right approach for your application, whether it's defining a series of API functions or using them only once.
You can quickly recall the functions in these two JavaScripts with the following example:
// Named function
function add(x, y) {
return x + y;
}
// Anonymous function
let myAdd = function(x, y) { return x + y; };
In JavaScript, functions can use variables outside the function body. W hen the function does this, we say it 'captures' these variables. As to why this can be done and the pros and cons beyond the scope of this article, a deep understanding of this mechanism can be helpful for learning JavaScript and TypeScript.
let z = 100;
function addToZ(x, y) {
return x + y + z;
}
Let's add a type to the function above:
function add(x: number, y: number): number {
return x + y;
}
let myAdd = function(x: number, y: number): number { return x+y; };
We can add a type to each argument before adding a return value type to the function itself. TypeScript automatically infers the return value type from the return statement, so we usually omit it.
Now that we've specified the type for the function, let's write out the full type of the function.
let myAdd: (x:number, y:number)=>number =
function(x: number, y: number): number { return x+y; };
The function type consists of two parts: the argument type and the return value type. B oth parts are required when writing out the full function type. W e write out the parameter types as a list of parameters, specifying a name and type for each parameter. T he name is just for added readability. We can also write this:
let myAdd: (baseValue:number, increment:number) => number =
function(x: number, y: number): number { return x + y; };
As long as the argument type is matched, it is considered a valid function type, not whether the argument name is correct or not.
The second part is the return value type. F
or return values, we use the symbol before the function and the return
=>
to make it clear.
As mentioned earlier, the return value type is a necessary part of the function type, and if the function does not return any values, you must also specify
void
type is void and cannot be left blank.
The type of function consists only of the argument type and the return value. T he capture variables used in the function are not reflected in the type. In fact, these variables are hidden states of functions that are not part of the API.
When you try this example, you'll find that if you specify a type on one side of the assignment statement but there is no type on the other, the TypeScript compiler automatically recognizes the type:
// myAdd has the full function type
let myAdd = function(x: number, y: number): number { return x + y; };
// The parameters `x` and `y` have the type number
let myAdd: (baseValue:number, increment:number) => number =
function(x, y) { return x + y; };
This is called "categorization by context" and is a type inference. It helps us better specify types for programs.
Every function argument in TypeScript is required. T
his does not mean
null
undefined
but rather that the compiler checks whether the user has passed in values for each parameter. T
he compiler also assumes that only these parameters are passed into the function.
In short, the number of arguments passed to a function must match the number of arguments expected by the function.
function buildName(firstName: string, lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // error, too few parameters
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result3 = buildName("Bob", "Adams"); // ah, just right
In JavaScript, each parameter is optional and can be passed on. W
hen there is no ginseng, its value is undefined. I
n TypeScript we can use
?
next to the parameter name T
he ability to implement optional parameters.
For example, we want last name to be optional:
function buildName(firstName: string, lastName?: string) {
if (lastName)
return firstName + " " + lastName;
else
return firstName;
}
let result1 = buildName("Bob"); // works correctly now
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result3 = buildName("Bob", "Adams"); // ah, just right
Optional parameters must be followed by required parameters. If we want first name to be optional in the example above, we have to adjust their position and put first name behind us.
In TypeScript, we can also provide a default value for a parameter when the user does not pass the parameter or
undefined
T
hey are called parameters with default initialization values.
Let's modify the example above to set the default value of last
"Smith"
function buildName(firstName: string, lastName = "Smith") {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // works correctly now, returns "Bob Smith"
let result2 = buildName("Bob", undefined); // still works, also returns "Bob Smith"
let result3 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result4 = buildName("Bob", "Adams"); // ah, just right
All parameters with default initialization after the arguments are optional and, like the optional parameters, can be omitted when the function is called. This means that the optional argument shares the parameter type with the default argument at the end.
function buildName(firstName: string, lastName?: string) {
// ...
}
And
function buildName(firstName: string, lastName = "Smith") {
// ...
}
Share the
(firstName: string, lastName?: string) => string
The default value of the default parameter disappears, retaining only the information that it is an optional parameter.
Unlike normal optional parameters, parameters with default values do not need to be placed after the required parameters. I
f an argument with a default value appears in front of the required parameter, the user must
undefined
value to get the default value.
For example, let's rewrite the last example
firstName
is an argument with a default value:
function buildName(firstName = "Will", lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // error, too few parameters
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result3 = buildName("Bob", "Adams"); // okay and returns "Bob Adams"
let result4 = buildName(undefined, "Adams"); // okay and returns "Will Adams"
The necessary parameters, the default parameters, and the optional parameters have one thing in common: they represent a particular parameter. S
ometimes you want to manipulate multiple parameters at the same time, or you don't know how many parameters will pass in.
In JavaScript, you can use
arguments
to access all incoming parameters.
In TypeScript, you can collect all the parameters into one variable:
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
The remaining parameters are treated as an unlimited number of optional parameters. Y
ou can have none of them, and you can have any of them. T
he compiler creates an array of parameters with the name you are omitting
...
.
Given a name later, you can use this array inside the function body.
This oddity is also used on the function type definition with the remaining parameters:
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;
this
Learning to use this in
this
is like an adult ritual. S
ince TypeScript is a superset of JavaScript, TypeScript programmers also need to figure
this
and be able to find out what's wrong when there's a bug. F
ortunately, TypeScript can notify you that this is used
this
I
f you want to understand how this works in
this
first read Yehuda
Katz's Understanding JavaScript Function Invocation and "this".
Yehuda's article
this
works internally, so let's do a brief introduction here.
this
arrow functions
In JavaScript,
this
is specified only when the function is called. T
his is a powerful and flexible feature, but you need to take a moment to figure out what the context of a function call is.
But as we all know, this is not an easy task, especially when returning a function or passing it as an argument.
Here's an example:
let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function() {
return function() {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
You
createCardPicker
is a function, and it returns another function. I
f we try to run the program, we will find that it does not pop up the dialog box but is wrong. B
ecause this in the function returned by
createCardPicker
this
set to
window
of
deck
object. B
ecause we just called
cardPicker()
T
he top-level non-method call
this
window
(Note: In strict mode,
this
undefined
window
To solve this problem, we can tie the correct this when the function is
this
I
n this case, no matter how you use it later, the bound 'deck' object is referenced. W
e need to change the function expression to use the ECMAScript 6 arrow syntax.
The arrow function can save the this value when the
this
not the value at the time of the call:
Let's change the function expression to using the lambda expression ( ( ) . This specifies the 'this' value when the function is created, not when the function is called.
let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function() {
// NOTE: the line below is now an arrow function, allowing us to capture 'this' right here
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
Better still, TypeScript will warn you of a mistake if you set the
--noImplicitThis
It will
this.suits[pickedSuit]
is
any
this
this
parameter
Unfortunately,
this.suits[pickedSuit]
is still
any
T
his
this
comes from a function expression in the object literal quantity. T
he method of modification is to provide an explicit
this
parameter.
this
is a false argument that appears at the top of the argument list:
function f(this: void) {
// make sure `this` is unusable in this standalone function
}
Let's add some interfaces,
Card
Deck
to the example to make type reuse clearer and simpler:
interface Card {
suit: string;
card: number;
}
interface Deck {
suits: string[];
cards: number[];
createCardPicker(this: Deck): () => Card;
}
let deck: Deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
// NOTE: The function now explicitly specifies that its callee must be of type Deck
createCardPicker: function(this: Deck) {
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
Now TypeScript knows that
createCardPicker
to be
Deck
a Deck object.
That
this
Deck
type,
any
--noImplicitThis
not mislisted.
this
argument is in the callback function
You can also see this error in
this
functions when you pass a function to a library function and it is called later. B
ecause when callbacks are called, they are called as a normal function,
this
be
undefined
W
ith a few changes, you can
this
with this parameter.
First, the author of the library function
this
type of this:
interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void;
}
this: void
means that
addClickListener
expects
onclick
to be a function that does not require a
this
type.
Second, annotate your calling code with
this
:
class Handler {
info: string;
onClickBad(this: Handler, e: Event) {
// oops, used this here. using this callback would crash at runtime
this.info = e.message;
};
}
let h = new Handler();
uiElement.addClickListener(h.onClickBad); // error!
After you
this
type, you explicitly declare that
onClickBad
Handler
instance. T
ypeScript then detects
addClickListener
the function to have
this: void
Change
this
type to fix this error:
class Handler {
info: string;
onClickGood(this: void, e: Event) {
// can't use this here because it's of type void!
console.log('clicked!');
}
}
let h = new Handler();
uiElement.addClickListener(h.onClickGood);
Because
onClickGood
this
type as
void
it
addClickListener
O
f course, this also means that you
this.info
.
If you want both, you have to use the arrow function:
class Handler {
info: string;
onClickGood = (e: Event) => { this.info = e.message }
}
This is possible because the arrow function does
this
so you can always pass them on to
this: void
void. T
he disadvantage is
Handler
object creates an arrow function. M
ethods, on the other hand, are created only once and
Handler
prototype chain.
They are
Handler
different Handler objects.
JavaScript itself is a dynamic language. It is common for functions in JavaScript to return different types of data based on different parameters passed in.
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x): any {
// Check to see if we're working with an object/array
// if so, they gave us the deck and we'll pick the card
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// Otherwise just let them pick the card
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);
let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
pickCard
method returns two different types depending on the incoming parameters. I
f an object is passed in to represent a card, the function is to grab a card from it. I
f the user wants to catch a card, we tell him what card he caught.
But how does this mean in a type system?
The method is to provide multiple function type definitions for the same function to overload the function. T
he compiler handles the call to the function based on this list.
Let's overload the
pickCard
function.
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
// Check to see if we're working with an object/array
// if so, they gave us the deck and we'll pick the card
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// Otherwise just let them pick the card
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);
let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
When this is changed, the overloaded
pickCard
performs the correct type check at the time of the call.
To enable the compiler to select the correct type of check, it is similar to the processing process in JavaScript. I t looks for the overload list and tries to use the first overload definition. U se this if it matches. Therefore, when defining overload, be sure to put the most precise definition first.
Note that
function pickCard(x): any
part of the overload list, so there are only two overloads here: one is the receiving object and the other receives the number.
Calling pickCard with other
pickCard
an error.