Books / TypeScript for Java Developers by CodeAhoy / Chapter 5
Functions
A function is a block of code that performs a specific task and can be called many times from different places in your code. Functions can accept parameters and may return a value.
Since TypeScript is primarily a functional programming language, functions are fundamental building block in JavaScript (classes are there but functional play key role in TypeScript.)
Using Functions
Here is an example of a simple function in TypeScript that takes two numbers as parameters and returns their sum:
function add(x: number, y: number): number {
return x + y;
}
let result = add(10, 20);
console.log(result) // Will output: 30
In the example above, the function add
and it takes two parameters, x
and y
of type number
. The function returns the sum of its two parameters, which is also a number
.
In Java, class methods are similar to functions in TypeScript. Here’s how we’d define the function above in Java:
public int add(int x, int y) {
return x + y;
}
int result = add(10, 20); // result will be 30
// Anonymous function
let add = function (x, y) {
return x + y;
};
Function parameters
Optional parameters
In TypeScript, function parameters are required and must always be provided when calling the function (just like in Java.) We can use ?
symbol to denote an function parameter that’s optional. Optional function parameters are assigned the value type undefined
.
function greet(name: string, greeting?: string) {
if (greeting) {
console.log(`${greeting}, ${name}!`);
} else {
console.log(`Hello, ${name}!`);
}
}
greet('Zayn'); // Outputs: "Hello, Zayn!"
greet('Zayn', 'Hola'); // Outputs "Hola, Zay!"
Default parameters
TypeScript also allows defining default values for function parameters called default-initialized parameters. The default value will be used if we do not provide the value or if the we passes undefined
.
function greet(name: string, greeting: string = 'Hello') {
console.log(`${greeting}, ${name}!`);
}
greet('Zay'); // Outputs: "Zay, John!"
greet('Zayn', 'Hi'); // Outputs: "Hi, Zay!"
Parameter Destructuring
As we learned, the destructuring assignment is a JavaScript expression that makes it possible to unpack values from arrays, or properties from objects, into distinct variables. This makes the code more readable by explicitly listing the names and types of the parameters being passed to the function (vs just passing an array) and makes it easier to work with individual variables.
You’d often see destructuring assignment in TypeScript functions. Let’s review a few examples.
function foo([a, b, c]: [number, number, number]) {
console.log(a, b, c);
}
foo([1, 2, 3]); // outputs: 1 2 3
In the example above, the function foo()
takes an array of three numbers as its parameters, and uses destructuring assignment to extract the values from the array into the variables a
, b
, and c
. We can work with these variables inside the function body easily.
We can also use destructuring assignment to extract the values from the object into variables, for example:
function bar({x, y, z}: {x: number, y: number, z: number}) {
console.log(x, y, z);
}
bar({x: 1, y: 2, z: 3}); // outputs: 1 2 3
Rest parameters
Sometimes, we may not know of how many parameters a function will ultimately require, or we might want to work with a group of parameters. In TypeScript, we can use rest parameters which allows us to represent an arbitrary number of parameters as an array. In other words, rest parameters are useful when we want to define a function that can accept any number of parameters.
To define a rest parameter, you use the ...
(ellipsis) followed by the parameter name. For example:
function sum(a: number, b: number, ...numbers: number[]) {
let total = a + b;
for (const num of numbers) {
total += num;
}
return total;
}
console.log(sum(1, 2, 3, 4, 5)); // prints 15
In the example above, the rest parameter numbers
is an array that contains all the parameters passed to the function after the first two parameters, a
and b
.
Note: We can also use the
arguments
object which is an array-like object that contains all the parameters passed to a function. However, in modern JavaScript applications, the rest parameters are preferred.
In Java we can achieve a similar effect by using Varargs. To define a varargs, we use the ...
syntax followed by the variable name. For example:
public int sum(int a, int b, int... numbers) {
int total = a + b;
for (int num : numbers) {
total += num;
}
return total;
}
System.out.println(sum(1, 2, 3, 4, 5)); // prints 15
Like rest parameters, varargs are represented as an array and allow you to define a method that can accept any number of arguments of a type. However, unlike rest parameters, varargs must be the last argument in the method signature.
Anonymous Functions
Normally, when we define a function, we give it a name e.g. function nameOfTheFunction(...)
. An Anonymous function is a function that doesn’t have a name of its own or an identifier. In the example below, we define an anonymous function (no name or identifier) and assign it to a variable so we can call it later.
const greet = function() {
console.log('Hello from an anonymous function!');
};
greet(); // prints "Hello from an anonymous function!"
We can also define anonymous functions and call them immediately. In the example below, we define an anonymous function and call it immediately by using the ()
syntax.
(function() {
console.log('Hello from an anonymous function!');
})();
Anonymous functions are often used as arguments to higher-order functions (functions that take other functions as arguments). E.g.:
const numbers = [1, 2, 3, 4, 5];
const squared = numbers.map(function(x) {
return x * x;
});
console.log(squared); // prints [1, 4, 9, 16, 25]
Here, the map()
is a higher-order function that takes a function as an argument and applies it to each element in an array. The anonymous function passed to map takes a single argument (x
) and returns its square.
In Java, Lambda Expressions are the closest thing to anonymous functions. Here’s a lambda expression that takes two arguments and returns their sum:
(int x, int y) -> x + y
Like TypeScript, Lambda Expressions are often used as arguments to higher-order functions or as a concise way to define functional interfaces (interfaces with a single abstract method).
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.forEach(n -> System.out.println(n));
button.addActionListener(e -> System.out.println("Button clicked!"));
In both of these examples, the lambda expressions are used as arguments to higher-order functions (forEach
and addActionListener
).
Arrow Functions
TypeScript has a shorter and more concise syntax for defining functions known as arrow function. Arrow functions are defined using the =>
symbol, which looks like an arrow and hence the name.
Here’s an example:
const add = (x:number, y:number) => {
return x + y;
};
console.log(add(2,3)) // Output is 5
Arrow functions can either be defined using blocks (as in the example above) or have concise body. Concise body have a single expression which becomes function’s implicit return value. We can write the function above more concisely as:
const add = (x:number, y:number) => x + y;
console.log(add(2,3)) // Output is 5
- Arrow functions do not have their own
this
. Instead, they they inherit the one from the parent scope. - Arrow functions cannot be used as constructors. If we call them with
new
, JavaScript will throw an error. - Arrow functions cannot use yield in their body and hence cannot be used as generator functions.
What is this keyword in JavaScript?
Function Overloading
Both TypeScript and Java allow function overloading, which allows you to define multiple functions or methods with the same name but with different parameters.
Here’s an example of function overloading in Java:
public class Calculator {
// Overloaded add method with two integer parameters
public int add(int x, int y) {
return x + y;
}
// Overloaded add method with two double parameters
public double add(double x, double y) {
return x + y;
}
// Overloaded add method with three integer parameters
public int add(int x, int y, int z) {
return x + y + z;
}
}
Calculator calc = new Calculator();
// Call the first add method with two integer parameters
int result1 = calc.add(10, 20); // result1 will be 30
// Call the second add method with double parameters
double result2 = calc.add(1.5, 2.5); // result2 will be 4.0
// Calls the third add method with three integer parameters
int result3 = calc.add(1, 2, 3); // result3 will be 6
In this example, the Calculator
class has three methods all named add
, but with different parameters:
- The first method takes two integers,
- the second method takes two doubles,
- the third method takes three integers.
When you call the add
method with different types and numbers of parameters, the Java compiler will automatically choose the right version of the method to execute based on the parameters you pass.
Now, let’s see function overloading in TypeScript. The example uses top-level functions that don’t belong to a class to illustrate that overloading works for both regular and class functions in TypeScript.
function add(x: number, y: number): number; // function signature for `add` with two numbers
function add(x: string, y: string): string; // function signature for `add` with two strings
function add(x: any, y: any): any { // implementation for add
return x + y;
}
// calls the add function with two numbers, result1 will be 30
let result1 = add(10, 20);
// calls the add function with two strings, result2 will be "hello world"
let result2 = add("hello", " world");
Here, the add(...)
function has two signatures with different parameter types (number
s and string
s). In function implementation, we simply adds the two parameters together and return the result. When we call the add(...)
function with different types of parameters, the TypeScript compiler will automatically choose the correct function to be used.