Table of Contents
As a programming language, JavaScript relies on the concept of First-Class functions, meaning that functions are treated like any other variable, such as a number, string, or array. One of the benefits of this is that functions can be passed into other functions, returned from functions, or assigned to variables to be called later if required.
This feature is heavily used in asynchronous code, where functions are often passed into asynchronous functions, often referred to as callbacks. But this can be tricky to use when we utilize TypeScript.
TypeScript offers us fantastic benefits of adding static types and transpilation checks, and it can help us better document what types of variables we expect into our functions — but what happens if we need to pass functions?
It seems fairly clear we need to type these functions, of course, but how do we type them, and how do we pass a function in TypeScript?
In this tutorial, you’ll learn about TypeScript functions and how to pass them as a parameter in your apps.
Typing simple variables is likely something most TypeScript developers are familiar with. But to construct a type for a function is a little more difficult.
A function type (note: this link redirects to old TypeScript docs, but it has a much clearer example than the newer ones) is made up of the types of the arguments the function accepts and the return type of the function.
We can illustrate a very simple example to showcase this:
const stringify = (el : any) : string => { return el + "" } const numberify = (el : any) : number => { return Number(el) } let test = stringify; test = numberify;
The above example, if implemented in JavaScript, would work fine and have no issues.
But now we’ve utilized TypeScript, errors throw when we try to transpile our code.
- Type '(el: any) => number' is not assignable to type '(el: any) => string'. - Type 'number' is not assignable to type 'string'.
The error message thrown here is pretty descriptive: the stringify
and numberify
function are not interchangeable.
They cannot be assigned interchangeably to the test
variable, as they have conflicting types. The arguments they receive is the same (one argument of type any
), but because their return types are different, we receive errors.
We could change the return types here to prove that our theory is correct.
const stringify = (el : any) : number => { return 1 } const numberify = (el : any) : number => { return Number(el) } let test = stringify; test = numberify;
The above code now works as expected, and the only difference is that we changed the stringify
function to match the type of the numberify
function. Indeed, the return type was breaking this example.
Interestingly, many other languages will create these function types based not only on the types of arguments and return types but also on the number of arguments for the function.
Let’s make one final example to expand on the last working example we had.
const stringify = (el : any, el2: number) : number => { return 1 } const numberify = (el : any) : number => { return Number(el) } let test = stringify; test = numberify;
Developers familiar with other languages might think that the above function examples aren’t interchangeable, as it’s often referred to as function overloading.
This example throws no errors, though, and it’s totally legitimate in TypeScript because TypeScript implements what’s referred to as duck typing.
It’s a small note, but it’s important to remember: the number of arguments isn’t utilized in type definitions for functions in TypeScript.
Now we know exactly how to construct types for our functions. We just need to ensure we type the functions that we pass in TypeScript.
Let’s work through a failing example together again.
const parentFunction = (el : () ) : number => { return el() }
The above example doesn’t work, but it captures what we need.
We need to pass it to a parent function a callback, which can be called later. So, what do we need to change here?
el
function argumentel
function (if it requires them)Upon doing this, our example should now look like this:
const parentFunction = (el : () => any ) : number => { return el() }
This specific example doesn’t require arguments, but if it did, here is what it would look like:
const parentFunction = (el : (arg: string) => any ) : number => { return el("Hello :)") }
This example is relatively simple in order to easily explain the concepts of TypeScript functions, but if you have more complicated types, you may spend a large amount of time typing everything.
The community maintains plenty of high-quality open source typings commonly used in TypeScript, called DefinitelyTyped, which can help you simplify and speed up the typing you will need to make use of.
I hope this article has been useful so you better understand the TypeScript landscape around passing functions as arguments to other functions.
Callbacks typically rely on this method, so you’ll often see heavy use of callbacks in any mature TypeScript codebase. Happy coding!
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Angular’s new `defer` feature, introduced in Angular 17, can help us optimize the delivery of our apps to end users.
ElectricSQL is a cool piece of software with immense potential. It gives developers the ability to build a true local-first application.
Leptos is an amazing Rust web frontend framework that makes it easier to build scalable, performant apps with beautiful, declarative UIs.
Learn more about the 5 best JavaScript libraries for dealing with multidimensional arrays, such as ndarray, math.js, and NumJs.