Functions are small blocks of code that take an input and either return an output or have side effects, which means that if
modifies variables outside the function. We need functions to organize code into small blocks that are reusable. Without functions, if we want to re-run a piece of code, we have to copy it to different places. Functions are critical to any TypeScript program. In this part of the series, we continue to look at different parts of TypeScript functions, including how to deal with the this
object with TypeScript, and overloading functions.
This Object
If this
is referenced in the regular function declared with the function
keyword, the this
object isn’t set inside an arrow function to the function that has the this
inside. If an arrow function is inside a constructor, then this
is set to the new object. If it’s not inside an object, then this
inside the arrow function is undefined
in strict mode. this
will be set to the base object if the arrow function is inside an object. However, we always get the window
object if we reference this
in an arrow function. For example, the following code wouldn’t compile and run since TypeScript doesn’t allow our code to have the global variable as the value for this
:
const fn = () => this
console.log(fn());
We get the window
object logged when console.log
is run. Likewise, if we ran this:
let obj = {}
obj.f = () => {
return this;
};
console.log(obj.f());
We get the same thing as we got before. This is in contrast to the traditional functions declared with the function
keyword. Let’s see what happens if we replace the functions above with traditional functions in the code above, as in the following code:
const fn = function() {
return this
};
console.log(fn);
Now, if we have noImplicitThis
flag on in our tsconfig.json
, we get the following error from the TypeScript compiler: 'this' implicitly has type ‘any’ because it does not have a type annotation.(2683)
We’re closer to getting our code working, but it’s still not going to compile. To fix this error, we need to put a fake parameter, this: void
, in the function signature, as in the code below:
const fn = function(this: void) {
return this;
};
console.log(fn);
With the code above, we make the this
variable unusable. If we want to use it for something, we can add an explicit type for this
instead of void
, to make it do something. For example, if we want to make a constructor object, we can write something like the following code:
In the code above, we created an interface called Person
to add a data type to the this
object in the greet
function in the person
object. When we call the greet
function, the this
parameter is ignored since we placed it as the first parameter. TypeScript only looks at the type of this
in the parameter and will not expect us to call the greet
function by passing in an argument for this
. After defining the person
object, we can then assign values to the name
and age
properties outside it. We already met the requirements listed in the interface when we’re defining the person
object, but we should also assign some meaning value to it so we can use the greet
function.
Then, when we run we run the console.log
in the last line of the example above, we get Hi Jane. You’re 20 years old
. We’ve successfully created a data type for this
, so there won’t be any ambiguity as to what the value of this
is. This helps developers understand what this
has in the code since this is one of the more confusing aspects of JavaScript. In plain JavaScript, this
can take on different values depending on where the this
keyword is located in the code. In traditional functions, this
’s value would be the function itself. Arrow functions do not change the value of this
, so whatever it was outside is the same as whatever it is inside the arrow function.
With TypeScript, we can’t use the this
and traditional functions to create classes. For example, if we write:
Then we would get the error Cannot find name ‘Person’. Did you mean ‘person’?(2552)
and the code won’t compile. TypeScript doesn’t let us use traditional functions as classes. To use make classes, we must use the class
syntax.
‘this’ Parameters in Callbacks
For callback functions used for event listeners, the callback functions that we pass in should be typed in an interface. For example, for setting the type of custom input control components, we can write something like Tthis:
interface InputElement {
addKeyUpListener(onclick: (this: void, e: Event) => void): void;
}
Then, when people that use the control, then they can write something like this for it to run:
What if developers that use the library try to reference this
, as in the following code?:
In this case, the TypeScript compiler will throw an error since this
is marked with the void
type in the InputElement
interface.
Function Overloads
Overloading a function is creating functions with the same name, but with different signatures. This isn’t allowed in JavaScript since functions are objects and we can’t re-declare the same object multiple times. However, since TypeScript is strongly typed, which is the opposite of the dynamic nature of JavaScript, it has to find a way to accommodate the dynamic aspects of JavaScript. To do this, TypeScript provides us a way to overload functions with different signatures. To overload functions with TypeScript, we just have to write multiple function signatures with the same name before defining the actual function with that name. For example, we can write something like this:
function getPerson(person: { name: string, age: number }): { name: string, age: number };
function getPerson(person: { name: string }): { name: string };
function getPerson(person: any): any {
return person;
}
With the code above, we have a getPerson
function that can either accept an object with the properties name
and age
, or just the property name
. The return type, which is after the colon, can either be an object with both the name
and age
properties, or an object with just the name
property. This is what we have in the first three lines of the code example above, where we just define the signatures we want for our getPerson
function.
Then in the actual getPerson
function definition, we have the actual function definition. We annotate the type of the parameter and return as any
, which is OK because we already defined the parameters and return types that the getPerson
accepts and returns respectively. We can call the getPerson
, as in the code below:
console.log(getPerson({ name: 'Jane', age: 20 }));
console.log(getPerson({ name: 'Jane' }));
From the console.log
statements above, we get:
{name: "Jane", age: 20}
{name: "Jane"}
If we try to call it with an argument that we didn’t define in the signature like: console.log(getPerson({}))
we get a No overload matches this call
error.
The main way to deal with the this
object in traditional functions is to pass it in as the first parameter of a function and then set a type to it by defining an interface for the type. TypeScript will ignore the this
parameter and treat it as if it doesn’t exist. It’s only used for setting the data type of this
. We can overload functions in TypeScript, which isn’t allowed in JavaScript. To do this, we just define different signatures for the function and give them the same name, then define the function with the same name after we defined the signatures that our function will accept.