Understanding various syntaxes to annotate a function's type in TypeScript
Collected in TypeScript
I often found myself second guessing while writing type annotations for TypeScript functions. To make it intuitive in future, I try to make sense of various syntaxes TypeScript provides in order to annotate functions.
Table of contents
The underlying principles
I was able to boil it all down to the following:
- Either individually annotate the function parameters and its return value with their respective types; or
- annotate the variable that holds a function with the function's type.
Detour - ways to define a function in JavaScript
Here is a probably non-exhaustive list.
Function declaration
This form requires that a name be given to the function.
function identity(v) {
return v;
}
Function expression
This form resolves to a function value which must then be bound to a variable, or must be used immediately, for example, as a callback.
// as a callback
[1, 2, 3].map(function (v) {
return v;
});
// assigned to a variable
const identity = function(v) {
return v;
}
An anonymous (or orphan(?)) function expression - without being used as a callback or assigned to a variable - is illegal at the top level, because it cannot be distinguished from a function declaration. So the following form is illegal at the top level:
>> function (v) {
return v;
}
Uncaught SyntaxError: function statement requires a name
A function expression can be named too.
const identity = function id(v) {
return v;
}
Arrow function expression
Unlike its function expression counterpart, an anonymous arrow function expression is legal, but useless, at the top level.
>> (v) => {
return v;
}
// Console output:
function (v)
length: 1
name: ""
// ...
An anonymous arrow function expression is best used as a callback.
[1, 2, 3].map((v) => {
return v;
});
An arrow function expression can be assigned to a variable.
const identity = v => v
Syntax #1 - annotate the function parameters and its return value
Most straightforward. All of the function forms can be annotated with this approach. But a function declaration can be annotated only through this approach.
Remembering the position of type annotations of the parameters is easy enough. But the return value type is placed between the parameter list and the body of the function.
For a function declaration and function expression, this means that the return type is placed between the parameter list and the opening brace {
of the function body. For an arrow function, it is placed between the parameter list and the arrow =>
.
// function declaration
function identity (v: number): number {
return v;
}
// function expression
[1, 2, 3].map(function (v: number): number {
return v;
});
// arrow function expression
const identity = (v: number): number => {
return v;
}
// arrow function expression as a callback
[1, 2, 3].map((v: number): number => {
return v;
});
Syntax #2 - Annotate the variable that holds a function expression
In this approach, simply annotate the variable name just like any other variable. Only that you have to annotate it with the type of the function expression.
This format is called function type expression, because they are used to type a function expression (and not a function declaration).
The format of a function type expression is confusingly similar to that of an arrow function expression: (param: type) => return_type
.
For a JavaScript function expression:
const identity = v => v
// or
const identity = function(v) {
return v;
}
annotate it by simply sticking the type expression between the variable name identity
and the =
preceding the function body:
const identity: (v: number) => number = v => v
// or
const identity: (v: number) => number = function(v) {
return v;
}
As I said before, a function type expression worsens readability because it is confusingly similar to an arrow function expression. We can separate the function type expression by defining it as a type:
type identityFn = (v: number) => number;
const identity: identityFn = v => v;
// or
const identity: identityFn = function(v) {
return v;
}
Now, this looks similar to any other variable annotation: const a: [string, number] = ["hello", 1]
.
Syntax #3 - annotate a function expression as an object type
More of an edge case, but worth distinguishing it from the rest.
A function is an object and can have properties. If you want to type a function as an object type, then you have to type the callable function with a format called call signature inside the object type. This format is similar to how you would annotate a function declaration's parameters and its return type.
type IIdentity = {
// call signature
(v: number): number,
// other properties, if any
whoAmI: "I am identity man"
}
// or
interface IIdentity {
// call signature
(v: number): number,
// other properties, if any
whoAmI: "I am identity man"
}
const identity: IIdentity = v => v;
identity.whoAmI = "I am identity man";
Written by Jayesh Bhoot