Functions in TypeScript

Functions in TypeScript

ddb2af52.webp

WHAT ARE FUNCTIONS?

Functions are the fundamental building blocks of any application in JavaScript. They’re how you build up layers of abstraction, mimicking classes, information hiding, and modules.

In TypeScript, while there are classes, namespaces, and modules, functions still play a key role in describing how to do things.

WHAT TYPESCRIPT ADDS?

Functions are the primary means of passing data around in JavaScript. TypeScript allows you to specify the types of both the input and output values of functions.

TypeScript also adds some new capabilities to the standard JavaScript functions to make them easier to work with.

SYNTAX AND STRUCTURE:

In TypeScript, function definition consists of a function name, its parameters with types, and its return type (which can be inferred by TypeScript).

DECLARE AN ARROW FUNCTION:

Following is the syntax to declare a function in TypeScript:

const functionName = (param1: string): string => {
  // ...
  return param1;
};

// functionName = Name of the function
// param1 = Name of the parameter (optional)
// (...): string = Return type of the function

In the above code, the syntax (param1: string): string => means a function with one parameter, named param1, of type string, that has a return value of type string.

If you don't specify the return type, TypeScript automatically infers the return type for you as shown below:

image.png

Here, const functionName: (param1: string) => string means, a function with a name functionName with one parameter named param1 of type string that has a return value of type string.

DECLARE A FUNCTION WITH function KEYWORD:

Declaring a function with the function keyword is almost similar to an arrow function.

Take a look at the following code:

function functionName(param1: string): string {
  // ...
  return param1;
}
// functionName = Name of the function
// param1 = Name of the parameter (optional)
// (...): string = Return type of the function

TYPE ANNOTATIONS:

When you declare a function, you can add type annotations after each parameter to declare what type of parameters the function accepts. You can add return type annotations to the function as well.

Parameter type annotations go after the parameter name. Return type annotations appear after the parameter list.

PARAMETER TYPE ANNOTATIONS:

Take a look at the following code:


const add = (a: number, b: number) => {
  return a + b;
};

console.log(add(1, 2));

In the above code, we declare an add function that accepts parameters a and b that is of type number.

So, if you try to break the rule and pass string instead of the number, you will get a compilation error as follows:

image.png

Also, if you try to add parameters more than what a function is accepting, you will get a compilation error, unlike JavaScript.

image.png

RETURN TYPE ANNOTATIONS:

Although we usually don’t need a return type annotation because TypeScript will infer the function’s return type based on its return statement. In some cases, you have to explicitly specify a return type for documentation purposes to prevent accidental changes or just for personal preference.

const returnANumber = (): number => {
  return 1;
};

Here, we are specifying that the returnANumber will return a value with a type number.

Now, if you specify the return type but don't return a value with valid type, TypeScript will throw a compilation error as follows:

image.png

IDE SUPPORT FOR RETURN TYPE:

The advantage of knowing the return type of a function is you get great IDE support if you want to do more operations on the value that is being returned.

For example,

let parseRange = (
  range: string
): {
  start: number;
  end: number;
} => {
  let [start, end] = range.split("-");
  return {
    start: parseInt(start),
    end: parseInt(end),
  };
};

let parsedData = parseRange("1-5");

In the above example, we have explicitly mentioned the return type of the parseRange function. However, even if we don't specify the return type, TypeScript will infer the return type for us.

If we try to access the start or end keys on the parsedDate variable, we will get autosuggestion from an IDE because TypeScript knows the return type of the parseRange function.

image.png

void RETURN TYPE:

Similar to languages like Java, void is used when there is no data. For example, if a function does not return any value then you can specify void as a return type or let TypeScript infer the return type as void.

const helloWorld = () => {
  console.log("Hello World");
};

let sayHello = helloWorld();

console.log(sayHello); // this will print undefined

You will get a compilation error if you attempt to return any value other than void after specifying void as a return type for that function:

image.png

You can only return undefined if you specify void as the return type. The following image shows valid and invalid return statements for a function with the return type of void.

image.png

Remember the Rule: "void is the return type of a function/method that doesn’t explicitly return anything".

When no return statement is provided and no explicit return type is specified in the function or method, it infers a void return type automatically.

FUNCTION AS A TYPE:

It is possible to assign a type to a variable that we want to use as a function with a specific parameter and return type. Here's how to declare one:


let add: (a: number, b: number) => number;

add = (a: number, b: number) => {
  return a + b;
};

In the above code, we are declaring a variable add and have assigned a type (a: number, b: number) => number (This is not an arrow function or a function declaration. Here, the left side of the => are the parameters that the function expects and the right side of the => is the return type of that function)

Using the add the variable we declared earlier, we can assign a function to it as a value.

TypeScript will throw an error stating that this variable only allows two parameters if we try to add another.

image.png

PASS Function AS A PARAMETER:

Take a look at the following code:

const sendMessage = (cb: (a: string) => void) => {
  cb("Hello, World");
};

sendMessage((str)=>{
  console.log(str);
});

Here, the sendMessage function has a parameter named cb (callback function) with one parameter named a, of type string, that has no return value, since the function has been declared to have a return type of void.

Even if you return anything inside a callback, TypeScript won't throw a compilation error, instead, it will ignore it.

image.png

DECLARE Function TYPE INSIDE AN object:

Earlier, we discussed the concept of an object type, which can hold a collection of different types. You can assign a function type to any of its keys.

GENERAL SYNTAX:

Take a look at the following code:

const person: {
  name: string;
  age: number;
  sayHi(): void;
} = {
  name: "Max",
  age: 30,
  sayHi: () => {
    console.log("Hi");
  },
};

In the above code, we have a person object with a key named say, and it is a function that returns nothing.

The following is a general way of defining a function type. The other way to declare a function in an object is with the property syntax.

PROPERTY SYNTAX:

The sayHi key in the above example can be declared in property syntax, which is more commonly used and readable.

const person: {
  name: string;
  age: number;
  sayHi: () => void;
} = {
  name: "Max",
  age: 30,
  sayHi: () => {
    console.log("Hi");
  },
};

The logic remains the same but this is a more readable and preferred format of assigning a function type.

unknown TYPE:

unknown is not commonly used but it can be helpful to be aware of it.

WHY USE unknown INSTEAD OF any:

unknown and any are similar to some extent but unknown is a bit more restrictive than any. To understand this take a look at the following code:

let userInput: unknown;
let userName: string;

userInput = 5;
userInput = "John";

In the above code, we are having userInput which is having types as unknown and userName with type string

Now, you can assign any value to the variable with the type unknown. So you won't get any compilation errors for assigning 5 and "John" to variable userInput.

But what happens if you try to assign userInput to userName?

let userInput: unknown;
let userName: string;

userInput = 5;
userInput = "Max";

userName = userInput; // This line will throw an error

image.png

It says Type 'unknown' is not assignable to type 'string even though string has already been assigned one line before.

Since the unknown type can only be assigned to any type and the unknown type itself, it can never be assigned to another type.

USE TYPE GUARDS TO AVOID ERRORS:

Adding type guards to the above code will allow it to work, as we saw in previous blogs.

let userInput: unknown;
let userName: string;

userInput = 5;
userInput = "Max";

if (typeof userInput === "string") {
  userName = userInput;
}

In the above code, we are only assigning userInput to userName only if userInput has a string type value.

DIFFERENCE BETWEEN unknown AND any:

What happens when you change the type of userInput from unknown to any?

let userInput: any;
let userName: string;

userInput = 5;
userInput = "Max";

userName = userInput; // compile without an error

The main difference between unknown and any is that unknown is much less permissive. We must perform some form of checking before performing most operations on values of type unknown, but we do not have to perform any checks before performing operations on values of type any.

never TYPE:

As the type name suggests, never refers to a function that will never return a value, not even an undefined or a null value.

DIFFERENCE BETWEEN never AND void:

  • The main difference between never and void is that void simply returns undefined if you try to access the return value from a function with void return type.

  • However, the function that returns never will cause the code to crash, and the code would not be executed further.

EXAMPLE 1 - FUNCTION THAT THROWS AN ERROR:

Generally, when we throw an error from any function, its return type is inferred to be never. Let's take a look at the following function:

const generateError = (message: string, code: number): never => {
  throw {
    message,
    code,
  };
};

const returnedValue = generateError("Something went wrong!", 500);
console.log(returnedValue);

We are throwing an error whenever we call the above function. This is what we see in the console:

image.png

We got the error we threw, but we didn't get any output from console.log(returnedValue), not even undefined. It indicates that a function with a return type never caused the script to crash.

EXAMPLE 2 - INFINITE LOOP:

Another type of function that returns never is the one with an infinite while loop.

const infiniteLoop = () => {
  while (true) {}
};

const returnedValue = infiniteLoop();
console.log(returnedValue);

In the above function, the infiniteLoop the function runs continuously, which breaks the script. If you hover over the infiniteLoop function, you'll see that TypeScript has inferred the return type as never.

image.png

CONCLUSION:

  • In TypeScript, functions are the building blocks of applications. In this article, we learned how to build type-safe functions using TypeScript, different types of annotations, how to use functions as types, and the concept of unknown and never types.

  • Having this knowledge will allow for more type-safe and easy-to-maintain functions throughout your code.

Make sure to subscribe to our newsletter on blog.wajeshubham.in and never miss any upcoming articles related to TypeScript and programming just like this one.

I hope this post will help you in your journey. Keep learning!

My Website, connect with me on LinkedIn and GitHub.