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.

Tslint Configuration with No implicity Any rule

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.

Error displayed when calling foo function

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
.

Nan is a number in JavaScript

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.

Getting variable type definition 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.

Error when passing bar as callback function

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.

The result from logging a 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).

Linting errors after changing type to 1

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.

Error because bar function return value type is not 1 but a number

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.

Did you like this article?

Share your thoughts by replying on Twitter of Become A Better Programmer or to my personal Twitter account.

Author