There are mainly two ways of defining functions in JavaScript. Namely:
JavaScript allows creating functions by defining them as expressions that can be assigned to a variable. Defining such function expressions can make use of the function keyword or via the use of Arrow functions.
- Function expressions
- Function declarations.
Let’s quickly go over how these two work.
Function expressions
For example, defining an add function can take either of these forms:
Using the function keyword:
Using Arrow functions, It takes the form:
Functions defined in either of these ways can be called like any other function:
let add = function(a,b) { return a + b }
Using Arrow functions, It takes the form:
let add = (a,b) => { return a + b }
Functions defined in either of these ways can be called like any other function:
console.log(add(1,2)) // logs 3
Function declaration
It looks like this:
function add(a,b) {
return a + b;
}
The above is a declaration, function declaration that creates an identifier, in this case, add, that can be later used as a function. The created add identifier can be called as follows:
console.log(add(1,2)) // logs 3
Now before we go ahead to see how to apply TypeScript’s type annotations, another important thing to be aware of, is the fact that functions in JavaScript are also objects!
Yes, a function in JavaScript is a JavaScript object.
This means the function add created above, either via function declaration or function expression, is also an object and can have properties assigned to it like any other object in JavaScript.
That is:
add.serial = "A01"
console.log(add.serial) // logs A01
For a more in-depth take on functions being objects, see Understanding Constructor Function and this Keyword in Javascript.
Knowing that functions are also objects is important, as this also influences the syntax that can be used for type annotation in TypeScript. We will see how later in the post.
Now that we have covered the basics, let’s now go into applying type annotations to functions.
We will start with function expressions.
Adding type annotations to function expressions is straight forward because the definition is an expression that is assigned to a variable.
Adding Type Annotations to Function Expressions
This makes it obvious where to place the type annotation, which is right after the variable declaration.
Knowing this, adding type annotation to the add function will look like this:
let add: (a: number, b: number) => number = (a,b) => { return a + b }
Notice that the actual function definition remains unchained. That is (a,b) => { return a + b } remains the way it was in the JavaScript version, and no type annotations are added to the function parameters, but TypeScript is able to infer the types based on type annotation ascribed to the variable.
That being said, it is also possible to update the function definition to have type annotations. That is:
In which case, the type annotation placed after the variable becomes redundant and can be removed, which leads to another way of typing the function expressions. This can be seen below:
let add: (a: number, b: number) => number =
(a:number,b:number):number => { return a + b }
In which case, the type annotation placed after the variable becomes redundant and can be removed, which leads to another way of typing the function expressions. This can be seen below:
let add = (a: number, b:number): number => { return a + b }
Ascribing type annotations to variables is possible when using function expression since function expression are assigned to variables. Function expression can also choose to have parameters in their definition annotated, although often time this is not needed.
Another important point is when ascribing types to variable holding functions, the syntax used resembles how Arrow functions are used, that is it makes use of "=>". And this is the only way to annotate variables with functions.
For example, this is correct:
While this leads to a syntax error:
The add JavaScript function defined via function declaration:
With TypeScripts type annotations applied becomes
Since there is no other way the TypeScript compiler can infer the types of the function parameters, the type annotation has to be supplied.
For example, this is correct:
let add: (a: number, b: number) => number
= (a,b):number => { return a + b }
While this leads to a syntax error:
let add: (a: number, b: number): number =
(a,b):number => { return a + b }
Adding Type Annotations to Function Declaration
function add(a,b) {
return a + b;
}
With TypeScripts type annotations applied becomes
function add(a: number, b: number): number {
return a + b;
}
Since there is no other way the TypeScript compiler can infer the types of the function parameters, the type annotation has to be supplied.
One might ask, what then is the type of the add function?
For example given a variable name defined as:
When the question is asked, what is the type of name it is easy to see it is string.
let name: string = “John Doe”;
When the question is asked, what is the type of name it is easy to see it is string.
When the same question is asked for the add function defined using function expressions, that is
It is easy to respond that the type is (a: number, b: number) => number
But what about the add function defined using function declaration?
let add: (a: number, b: number) => number = (a,b) => { return a + b }
It is easy to respond that the type is (a: number, b: number) => number
But what about the add function defined using function declaration?
To help answer this question we can use the combination of an IDE and the typeof operator of TypeScript. The typeof operator when used in the type signature context can help extract the type of a value.
So to answer the question, what is the type of the add function defined using function declaration, we use typeof on add, in the type signature context and using a tool that offers IntelliSense, in this case, the TypeScript playground, we can see what the type is:
Remember we mentioned that functions are also objects. And we showed how we can add properties to functions as we do to objects. Well, functions being objects also provide us another way of supplying type information about functions.
And as can be seen above, the type of add when defined using function declaration is (a: number, b: number) => number, which is exactly the same type annotation of the same function when defined using the function expression!
Typing functions using Call signature of an object literal type
A question a curious reader might ask upon being told that functions are objects in JavaScript is this: if functions are objects, how come we can call them? How come functions can be called by appending () to the end of the function? That is something like functionName().
The answer to that question is in realising that the syntax functionName() is really a syntactic sugar for either functionName.call() or functionName.apply(). That is, calling a function, is really nothing but assessing the apply or call property of the object representing that function.
See MDN entries for Function.prototype.apply() and Function.prototype.call() for more information.
This knowledge helps in understanding another way of typing functions, which is using the call signature. Doing that builds on how object literal can be used to specify types.
For example to provide type annotation describing an object with a property name of type string, and property age, of type number, the following interface can be created and used:
The type annotation outlines the property name together with a type annotation.
Knowing this, and knowing that functions are also objects, that can be called via a call, or apply property then we can provide a type annotation to our add function as shown below:
interface Person {
name: string
age: number
greet(): string
}
let john: Person = {
name: "John Doe",
age: 20,
greet() {
return “hello world”
}
}
The type annotation outlines the property name together with a type annotation.
Knowing this, and knowing that functions are also objects, that can be called via a call, or apply property then we can provide a type annotation to our add function as shown below:
interface Adder {
apply(a: number, b: number): number
call(a: number, b: number): number
}
let add: Adder = (a: number, b: number) => { return a + b }
We can go one step further in the definition of Adder by removing the need to specify apply and call explicitly. This is because as we already know that a function (which is an object) can have its apply and call property called without having to explicitly specify them. That is, the call signature of a function is a syntactic sugar that will expand to explicitly use either apply or call. We can apply this knowledge of the call signature to the type definition by removing the apply and call. Doing that we end up with:
This way of providing type annotations to functions is usually referred to as using the call signature of an object literal type.
interface Adder {
(a: number, b: number): number
}
This way of providing type annotations to functions is usually referred to as using the call signature of an object literal type.
It is worth noting that in TypeScript, the keyword type and interface are interchangeable in most cases hence, the above can also be defined using type instead of interface.
Summary
- The way functions are typed in TypeScript depends on the ways functions can be created in JavaScript.
- Functions can be created either via function declaration or function expressions.
- They are two main ways to ascribe type annotations to functions. Typing the parameters and return type of the function, or typing the variable that holds the function.
- Functions defined using function declaration can only be typed by providing type annotation to parameters and return value. A function expression can be typed by providing the type annotation to the variable that holds the function expression. Also, it is possible to ascribe type to the function parameters defined in the function expression, this is usually redundant. It is only needed in cases where the compiler cannot infer their types based on the type annotation ascribed to the variable.
- When typing a variable that holds a function, the type annotation makes use of => to specify the return type. Using this Arrow function style is the only way to type variables holding function expressions.
- Also, Functions are just objects! And this influences the third way of typing functions which is called: call signature using object literals.
I am writing a book: TypeScript Beyond The Basics. Sign up here to be notified when it is ready.
No comments:
Post a Comment