Functions are one of the most used language features, not just in JavaScript function in almost all the languages. In layman’s terms, they are called subroutines. Writing a function in JavaScript is very simple. You can either use the function declaration syntax using the function
keyword or a function expression syntax that returns a new function.
In the above example, we have declared a function with the name sum
that accepts argument a
and b
. It performs arithmetic sum operation on these two operands and returns a result.
In an ideal situation, these two arguments must be numbers or values of the type number
to be precise. But JavaScript doesn’t stop you from passing any other values as arguments to the function call. That’s why sometimes you can see NaN
as the return value when arithmetic operations are performed on non-number
values.
Also, we expect the return value of this function to be a number
but JavaScript won’t stop you from treating it differently. For example, in the above program, we have called split
method on the expected return value of type number
but is a prototype method of a string
value. Luckily, the return value was a string
(not ideal though) and our program did not crash.
As you can see, the program above is not ideal to run in mission-critical situations. We need to know if our program can work safely before it is deployed to the mission. This is where TypeScript can help us. Before compiling the program, TypeScript walks through each function call of sum
and checks if arguments are all right. Let’s redesign the program above.
The above program could not compile because the sum
function expects both the argument values to be of the number
type and TypeScript will point out where the sum
call was made with either insufficient arguments or argument values of wrong types.
Since we have explicitly mentioned that the sum
function returns a value of type number
, TypeScript won’t allow non-number
specific operation on it. For example, the .split()
call on the return value is not permitted since it is a prototype method on a string
type and not a number
type.
TypeScript won’t also allow wrong API consumption. For example, if you call the sum
function with more than two arguments, TypeScript considers that as an error since definitions of other function parameters are not provided.
A function can also be written as an expression. A function expression syntax returns a function rather than declaring a function that’s why we need to it inside a variable or constant. Functions are first-class objects in JavaScript, which means they can be stored in a data structure (or simply in a variable), passed as a function argument value, or returned from a function as a value.
A function expression returns an anonymous function, which means it doesn’t declare itself to the world (global scope) as in the function declaration syntax. You can also use the ES6 (fat) arrow function expression syntax as shown below which is better in my opinion for this purpose.
Function Type
In TypeScript, when you declare a variable and assign a value to it in the same statement, TypeScript annotates the variable with the type it receives from the value. This is called type inference. Hence we do not need to provide a type for a variable if we have provided the value in the variable declaration syntax.
So in the above example, since we did not explicitly provided a type for the constant sum
, what is the type inferred by the TypeScript from the function expression (on the right-hand side).
💡 If you move your mouse cursor over an entity, constant
sum
in this case, VSCode will show you the type of that entity.
As you can see from the above screenshot, the type of the variable sum
is a function of type (a: number, b: number) => number
. This might resemble ES6 arrow function syntax, but this is how a function type is represented in TypeScript, no matter how a function is created.
In the above example, we have first created a variable sum
of type function that accepts two arguments of type number
and returns a value of type number
. After its declaration, we have assigned it with a function value. You can avoid annotating this function value with types since TypeScript can infer that from the type of variable (inference acts in both directions).
Function types are useful when a value of a specific function signature is expected. Let’s imagine we have _add
and _subtract
functions but they can’t be invoked directly. You need to call getOperation
function that returns one of these functions based on the argument value.
In the above program, we have defined the function _add
and _subtract
using the arrow expression syntax as we would normally. The getOperation
function returns _add
function as the returned value if the operation
argument (string
) value is 'add'
. When a function returns another function, it is called currying function.
In the above example, the getOperation
function returns a value of type Function
which represents all the function values. It’s like any
but just for the functions. This comes from JavaScript since every function value is an instance of Function
class.
Therefore a value of type Function
lacks shape and TypeScript would let it invoke in whichever way possible. Hence we could invoke the return value of getOperation
function with undefined
argument. What we need a concrete type of the function value returned by the getOperation
function.
Now we have specified the concrete type for the return value of the getOperation
function. This does two things for us. First, the return value of the getOperation
must be (a: number, b: number) => number
which is not the case with the _add
function at the moment. And second, TypeScript would check the invocation of this returned value. In the above case, we have invoked the returned function with a string
argument which is invalid.
As you can see, we are repeating the same function type again and again and quite honestly, this looks a little hard to understand. Let’s use type alias to simply it. A type alias is just liked a variable declaration but it stores a type rather than a value and it is declared using type
keyword. A type alias can only be used as a type and not as a value.
In the above example, we have created a type alias ArithmeticFunc
and used it in the places where this type is required.
💡 In the above example, we have specified types of the functions
_add
and_substract
explicitly. This could be useful if you do not want TypeScript to infer types from function expression (RHS).
Optional and Default Parameters
In some cases, some arguments of a function are not required. In these cases, we need to instruct TypeScript that some arguments are optional. This is done by providing ?
prefix in the type annotation of the parameter.
In the above example, the canDrive
parameter is optional hence we can call the function without providing a value for it. But if the value for it is provided, then it must satisfy the type as described by the parameter.
Since optional parameters (arguments) can be skipped in the function call, they all must appear after all required arguments. We can also make a function parameter (argument) optional implicitly by assigning a default value in the function definition (or expression).
In the example above, the canDrive
argument is optional since it has a default value. Since parameters with a default value are optional implicitly, they all must appear after all required arguments.
💡 The
:boolean
type annotation is optional since TypeScript can infer it from the default value oftrue
.
Rest Parameters and Spread Operator
If a function accepts an arbitrary number of arguments, then we use ...param
syntax in JavaScript. Such functions are called variadic functions. Here, the param
ar runtime will be an array that contains all the argument values passed by an invoker.
We can collect all the argument values passed in the function call in this array or exclude the initial few as shown in the below example. Since this collects the rest of the arguments, it is called the rest parameters syntax. Since this is an array, we need to specify a valid array type for it as well.
In the above example, the first argument is received normally while the rest arguments are received in the books
array. Also, the function expects all the remaining arguments after the name
to be values of the type string
, that’s why the books
needs to have the type of string[]
or Array<string>
.
You can use any[]
in dynamic situations where the type of argument value could be anything. You can also use the ...
syntax to spread the values of an array. So for example, if you had an array of things
, instead of passing one item of this array at a time (_using _thing[index]
_), you can spread it in the printThings
function call. For that reason, it is called the spread operator.
💡 The spread operator is a JavaScript feature (landed in ES6 specifications).
Function Overloading
If you are familiar with OOP, then you must have heard about method overloading. In some programming languages like Java and C++, you can define multiple class methods with the exact same name but different parameters. Based on the number of arguments and their types in the function call, an appropriate method is executed at runtime.
JavaScript can’t provide method overloading or function overloading because it is not a statically typed language. In the end, you can’t have multiple functions or methods with the same name in the same scope in JavaScript.
But TypeScript supports function overloading as long as the number of arguments stays the same for all the functions. Function overloading is a similar concept but it only exists in TypeScript which means only at the compilation time.
In the above example, we have defined a concatenate
function that takes two arguments of any
type and returns a value of any
type. Since the runtime-type of argument values can be anything (depending on the arguments in the function call), you need to check the runtime-type of these arguments inside the function body. Similarly, you can return anything as the return value.
To provide the TypeScript compiler some information about expected argument types and return type of the function when arguments are provided in that specific order, we declare functions with the same name but without the body. By looking at these function types, TypeScript can interpolate expected return value of the concatenate
function call.
The function without a body is called overloaded signature and function with body is called implementation signature. The overloaded signature must precede the implementation signature. Since this is only a compile-time feature, the overloading signature only exists at runtime.
Function overloading is great when the return value depends on the signature of the function call. Since if you use any
or a union type as the return value, you need to manually assert the return type which could lead to runtime errors. However, function overloading it a little too much if you are working with simple types. Instead, use generics, they are awesome.