Oloruntobi Awoderu Passionate software engineer thriving on product-oriented and efficient solutions to problems that aid people in their everyday lives.

The definitive guide to typing functions in TypeScript

5 min read 1584

JavaScript is one of the world’s most popular programming languages, and the language of the web. However, because it’s not strongly typed, there is a possibility of introducing bugs into our code inadvertently. This is where TypeScript comes in.

TypeScript is a strongly typed subset of JavaScript, and it provides the necessary tooling to create type guards for our application. Functions are one of the main paradigms in both JavaScript and TypeScript, and the building blocks of any application. Therefore, it is essential for TypeScript developers to understand how to build and use strongly typed functions in their day-to-day work.

This article is a guide on how to create and use strongly typed functions. We will use plenty of examples to outline the many functions TypeScript devs can build.

Contents

Prerequisites

In order to be able to follow this tutorial, you’ll need:

  • Working knowledge of JavaScript and TypeScript
  • An environment where you can run TypeScript examples, either by installing TypeScript on your local machine, or the official TypeScript playground

Why typed functions are important

When we build applications, our aim is always to make them stable, reliable, available, maintainable, and scalable. Having and using typed functions helps us move closer to that ultimate goal.

Typed functions help us catch bugs that we might miss without type guards. TypeScript, and by extension typed functions, have helped many companies reduce bugs that ship with their product, helping them reduce development or support time that could have resulted otherwise.

Creating functions and type guards

In this section, we are going to be creating functions and then adding type guards to them.

Below is a simple function called subtraction:

function subtraction(foo, bar){
  return foo -  bar;
}

This subtraction function takes in two parameters: foo and bar, and then returns the difference between them.

When we paste this code within our TypeScript playground, we get an error about our parameters having an implicit type of any.



subtraction function error

We’ll need to add types to our parameters as seen below:

function subtraction(foo: number, bar: number){
  return foo -  bar;
}

We have added a number type for our parameters above. This makes sure that our parameters are number values. If we try to assign a parameter that isn’t a number to this function (as I have done below), we’ll get an error: Argument of type 'string' is not assignable to parameter of type 'number':

subtraction(10, 'string')

TypeScript implicitly recognizes the type for the return value from the type of the params, but we can also add a type to the return value. This is shown in the example below:

function subtraction(foo: number, bar: number): number{
  return foo -  bar;
}

We can also declare functions using type interfaces. Here, we declare an attribute interface to pass into the functions:

interface Attribute {
  age: number,
  sentence: string
}

function personality(attribute: Attribute): string{
  return `${attribute.sentence} ${attribute.age}`;
}

const attribute: Attribute = {
  age: 18,
  sentence: "My age is"
}

const getPersonality = personality(attribute);

If we paste this code into our TypeScript playground, we’ll see it passes our type checks and no errors are shown.

Typed arrow functions

Let’s now turn to typed arrow functions, and how to create them. The syntax for adding types to arrow functions is very similar to that of normal functions.

I’m going to refactor our last function into an arrow function and apply type guards to it:


More great articles from LogRocket:


interface Attribute {
  age: number,
  sentence: string
}

const personality = (attribute: Attribute): string => {
  return `${attribute.sentence} ${attribute.age}`;
}

const attribute: Attribute = {
  age: 18,
  sentence: "My age is"
}

const getPersonality = personality(attribute);

The same rules that apply to normal functions also apply to arrow functions.

Asynchronous typed functions

Now that we have learned how to create normal and arrow functions, we are going to look at how to create asynchronous typed functions. There’s a difference between typing asynchronous functions and normal functions: the return type of an async function must be the Promise<T> generic.

This generic represents the promise object being returned by the async function, where the <T> represents the type of the value to which the promise resolves. Below is an example of a typed asynchronous function:

interface Fruit {
  id: number,
  name: string
}

const fruits: Fruit[] = [
  {id: 1, name: "apple"},
  {id: 2, name: "Orange"}
]

async function getFruitById(fruitId: number): Promise<Fruit | null> {
  const findFruit = fruits.find(fruit => fruit.id === fruitId);
  if(!findFruit) return null;
  return findFruit;
}

async function runAsyncFunction() {
  const getFruit = await getFruitById(1);
}

In the above example, we created a fruit interface to handle the types for the params. We also created the getFruitById function, and set the return type to be the Promise generic with Fruit or null as the type of the return value. Finally, we had to wrap the getFruitById call into the runAsyncFunction, because we cannot use await in the top level of a file or we’ll get the the error below from the TypeScript compiler.

Fruit function error

Wrapping it in another async function fixes this error.

Optional parameters

We’ve seen how to pass typed parameters into typed functions from our examples above, but these cases only cover the instances in which we are sure about the number and exact type of each parameter. There are times in which we aren’t sure, and we need to cater to such a situation as well:

interface Fruit {
  id: number,
  name: string | null ,
  type?: string
}

Above is an example of declaring optional parameters using an interface. The name param can take a value that has a type string or null.

We use the ? to make the type param optional, which means we don’t need to pass the param into our function, and when we do pass it, the value has to be a type of string.

We can also declare the params and types directly inside the function as seen below:

function returnUser(firstName: string, lastName?: stringl) {
  return `My name is ${firstName} ${lastName}`;
}

In the above example, the firstName param is required, while the lastName param isn’t, but if passed, must be a type string.

We can also define types for rest parameters. Rest parameters are a feature in JavaScript that passes many parameters as a single array into the function. The example below shows how to do this:

function addition(...args: number[]) {
  let result: number = 0;
  for(let i = 0; i < args.length; i++){
    result += args[i];
  }
  return result;
}

The args param is an array of number types; this helps us typecheck our args params and prevents us from passing any param that isn’t a type of Number as seen in the image below.

args param error

The TypeScript compiler throws an error if the type of the param passed isn’t a number.

Function overloads

The number and type of arguments can vary when calling certain JavaScript functions. An example would be writing a function that returns a user from either an ID (one argument) or phone number (one argument), or a combination of address and name (two arguments).

The overload signatures in TypeScript permit a function to be called multiple ways. This is achieved by writing a few function signatures (usually two or more), then the body of the function, like so:

interface User {
  id: number;
  phoneNumber: string;
  fullName: string;
  address: string;
};

const users: User[] = [
  { id: 1, phoneNumber: "08100000000", fullName: "First User" , address: "Santa fe, florida" },
  { id: 2, phoneNumber: "08111111111", fullName: "Second User", address: "San Fransisco, California" }
];

function getUser(id: number): User | undefined;
function getUser(phoneNumber: string): User | undefined;
function getUser(address: string, fullName: string): User | undefined;

function getUser(idOrPhoneNumber: number | string, address?: string): User | undefined {
  if (typeof idOrPhoneNumber === "string") {
    return users.find(user => user.phoneNumber === idOrPhoneNumber);
  }

  if (typeof address === "string") {
    return users.find(user => user.address === address);
  } else {
    return users.find(user => user.id === idOrPhoneNumber);
  }
}

const userById = getUser(1);
const userByPhoneNumber = getUser("08100000000");
const userByAddress = getUser("San Fransisco, California", "Jon Doe");

In the code above, we declare the input parameters and types that will be passed into the function in a UI. In addition, two overloads accept one argument, while one overload accepts two arguments; these are referred to as overload signatures.

We then developed a function implementation with a compatible signature. Each function has an implementation signature, but this signature cannot be directly called.

Statically typing constructors

Class constructors are very similar to functions in the sense that we can add parameters with different types, default values, and overloads. We can also type our constructors using a normal signature with default values or overloads.

Normal signature with overloads:

    class Example {
      a: number;
      b: number;

      constructor(a = 1, b  = 4) {
        this.a = a;
        this.b = b
      }
    }

Overloads:

class Example {
        constructor(a:number, b:string);
     constructor(xs: any, y?: any) {
        }
}

There are a couple of differences between typed functions and typed constructors:

  1. Typed constructors can’t have typed annotations, as the class instance is always what is returned
  2. Typed constructors can’t have typed parameters, as these belong to the outside class

Conclusion

In this article, we’ve gone through the various types of typed functions, including how to create and use them. Typed functions help us create safeguards, and write code that is highly reliable and easy to debug. I hope this article has given you some more context for why strongly typed functions are imperative to understand as a TypeScript developer, and information that can help you build faster, safer, and better TypeScript applications.

Writing a lot of TypeScript? Watch the recording of our recent TypeScript meetup to learn about writing more readable code.

TypeScript brings type safety to JavaScript. There can be a tension between type safety and readable code. Watch the recording for a deep dive on some new features of TypeScript 4.4.

Oloruntobi Awoderu Passionate software engineer thriving on product-oriented and efficient solutions to problems that aid people in their everyday lives.

One Reply to “The definitive guide to typing functions in TypeScript”

  1. You can call async function directly using IIFE

    (async function(){
    const getFruit = await getFruitById(11);
    console.log(getFruit)
    })();

Leave a Reply