Since the moment TypeScript was developed it has helped JavaScript developers to catch mistakes because of the lack of type definition and rules defined when using compilerOptions
inside a tsconfig.json file. One common pattern in JavaScript is to pass callback functions as parameters when triggering other functions. In this article, you are going to learn how to do the same in TypeScript.
Similar to JavaScript, to pass a function as a parameter in TypeScript, define a function expecting a parameter that will receive the callback function, then trigger the callback function inside the parent function. For instance, notice how we pass bar
as a callback function when calling the foo
function in the following example:
function foo(callback) {
console.log('foo() function called!');
callback();
}
function bar() {
console.log('bar() function called!');
}
foo(bar);
However, there is a problem with this code. We are not fully leveraging the power of TypeScript. If you copy this code and paste it into a JavaScript file, there wouldn’t be any syntax errors as we are not using any TypeScript types.
Also, we run into the possibility of passing any parameter value, which means we can provide a string rather than a function as an argument of foo
without “getting errors” until the code is executed, a mistake that could easily happen if using JavaScript.
const a = '';
foo(a);
Depending on the TypeScript Tslint configuration, we could enforce rules such as not allowing parameters to have implicitly have any
type.
Table of Contents
Define parameter type using the Function
type
At this point, we haven’t solved the problem of defining the type for callback
, at least this logic will fail at build time, instead of runtime.
To solve the issue the following error:
Parameter 'callback' implicitly has an 'any' type.ts(7006)
Define the type of the callback
parameter as Function
.
function foo(callback: Function) {
console.log('foo() function called!');
callback();
}
Now, if we try one more time to pass something different than a callback function, TypeScript will point out the error in the statement where foo
is triggered.
Define the expected value returned from the Function
type
Using the Function
type is a big step in preventing common mistakes due to the lack of type definition seen in JavaScript. However, using the Function
type still leaves room for potential errors in the logic.
What if the function receiving the callback function uses the returned value from the callback function to run an additional process? To make this assumption more clear, let’s make changes to the foo
function.
function foo(callback: Function) {
console.log('foo() function called!');
const number = callback();
return Math.sqrt(number);
}
Notice the foo
function not only calls the callback
function, but also uses the value returned from the callback function to get the square root of the value, assuming the value returned by the callback function is a number.
If you look back the definition of the back
function, this function is not returning anything, and when nothing is returned in a function JavaScript defaults the returned value as undefined
.
// bar function returns 'undefined'
function bar() {
console.log('bar() function called!');
}
Therefore, if we pass bar
as the callback
function of foo
, there might not be any errors during runtime, but there could be unexpected behavior from triggering this process.
function foo(callback: Function) {
console.log('foo() function called!');
const number = callback();
return Math.sqrt(number);
}
function bar() {
console.log('bar() function called!');
}
console.log(foo(bar));
// Expected result: NaN
Instead of expecting a number from calling foo(bar)
, the real result will be NaN
which stands for
“Not-a-number”. Ironically Nan
is considered a number in JavaScript regardless of its definition.
Having said that, the final result is a number NaN
, which is not a number. Unless we take into account NaN
a possible option in our logic, the human brain is not “wired” to think of NaN
as a number. Hence, developers will typically expect a real number.
Luckily we can prevent this unexpected behavior in TypeScript by defining the type of the expected returned value from the callback function. Unfortunately, we no longer can use the type Function
for this solution. Instead, use an arrow function expression that returns a type to provide a valid the type definition. Let’s modify one more time the definition of the foo
function to understand this concept.
function foo(callback: () => number): number {
console.log('foo() function called!');
const number = callback();
return Math.sqrt(number);
}
The syntax () => number
can be confusing at first, but it tells the compiler that the callback
parameter must be a function that returns a number.
You can do this with different types besides the number
type, even custom objects.
function A(callback: () => number) { }
function B(callback: () => string) { }
function C(callback: () => Array<string>) { }
function D(callback: () => Array<number>) { }
function E(callback: () => Object) { }
function F(callback: () => { firstName: string; lastName: string }) { }
If we go back to our updated foo
function and hover over the variable number
, you will see it expects the variable to have a type of number
.
Now, we should get compilation errors if we try to call foo(bar)
.
Argument of type '() => void' is not assignable to parameter of type '() => number'.
Type 'void' is not assignable to type 'number'.
What’s even better, this will let our IDE tells us instantly where we have errors in the code.
As you might think, to solve this error we need to update the bar
function to return a number.
function bar() {
console.log('bar() function called!');
return Math.random();
}
Now, there shouldn’t be any errors when running the following code,
function foo(callback: () => number): number {
console.log('foo() function called!');
const number = callback();
return Math.sqrt(number);
}
function bar(): number {
console.log('bar() function called!');
return Math.random();
}
console.log(foo(bar));
and it should log the square root of a random number.
Bonus: Define Custom Types returned from a callback function
Finally, this is a bonus section to master how to pass a function as a parameter. It is possible to define custom value types returned by a callback function. For instance, we can think of callback
as only returning 1
.
function foo(callback: () => 1): number {
console.log('foo() function called!');
const number = callback();
return Math.sqrt(number);
}
However, as soon as we make this change we get linting errors when calling foo(bar)
.
Isn’t bar
supposed to return a number?
That’s correct. The bar
function is expected to return a number. However, foo
is expecting the callback
parameter to be a function that returns the value of 1. You might think of this as an odd case, but there are scenarios where you want to expect specific values.
This means, the bar
function needs to be updated to return the type of 1
.
function bar(): 1 {
console.log('bar() function called!');
return 1;
}
Notice that this will fail if the type of the value returned from the bar
function is number
, even if the function returns 1
.
For a better solution, generate a new type with all the possible custom values accepted by the callback
function.
type AcceptedNumbers = 1 | 2;
Once the type is defined, it is only a matter of updating every function where it is needed.
type AcceptedNumbers = 1 | 2;
function foo(callback: () => AcceptedNumbers): number {
console.log('foo() function called!');
const number = callback();
return Math.sqrt(number);
}
function bar(): AcceptedNumbers {
console.log('bar() function called!');
return 1;
}
console.log(foo(bar));
Conclusion
In this article, you learned how to pass a function as a parameter in TypeScript, starting with passing a generic callback function using the type Function
, and also learning how to pass parameter functions returning specific types of values and even custom objects. This helps our code to not only be more predictable but prevent unexpected behaviors often caused by JavaScript’s lack of type definition.
More TypeScript Tips!
Since you read this article, I thought you might be interested in reading other TypeScript articles I’ve written that you might be interested in checking out.
- TypeScript | The Unknown Type Guide
- TypeScript | Organizing and Storing Types and Interfaces
- TypeScript | Double Question Marks (??) – What it Means
- TypeScript | Objects with Unknown Keys and Known Values
- TypeScript | Union Types – Defining Multiple Types
- TypeScript | Declare an Empty Object for a Typed Variable
- TypeScript | Union Types vs Enums
- TypeScript | Convert Enums to Arrays
Did you like this article?
Share your thoughts by replying on Twitter of Become A Better Programmer or to my personal Twitter account.