Functions in TypeScript
Function expressions, named function syntax, or the succinct arrow function (lambda) syntax are the three ways that functions can be defined in TypeScript. One of the main features is that the expected types of parameters and the function’s return value can be explicitly defined using type annotations.
The parameters, return type, and function name are all specified in a function declaration. For the return type, TypeScript conducts type inference if it is able to identify the types from the implementation logic.
Basic Function Definition and Typing
Just like you can annotate other variables, you can also annotate function arguments. A colon (:
) is used to divide the parameter list from the return type declaration, which comes right before the function body.
In the example below, x
and y
are explicitly typed as number
, and the function’s return value is explicitly typed as number
.
function sum(x: number, y: number): number {
return x + y;
}
// Usage
console.log(sum(84, 76));
// console.log(sum('84', 76)); // Error: Argument of type '"84"' is not assignable to parameter of type 'number'.
Output:
160
Compile-time errors occur when arguments that do not match the defined types are attempted to be passed.
If a function does not return a value, the return type annotation should be explicitly set to :void
. The void
type signifies that a function does not have a usable return value. Functions declared with a :void
return type can implicitly or explicitly return undefined
, but cannot explicitly return other values.
function logMessage(message: string): void {
console.log(message);
}
logMessage("Starting calculations...");
Output:
Starting calculations...
Function Types
Using a function type, also known as a call signature, you can specify the static type of a variable or parameter that will store a function value. Functions can now be assigned to variables, supplied as arguments, and returned from other functions, enabling them to be regarded as first-class objects.
A function type’s syntax is very similar to that of an arrow function (=>
):
type FunctionName = (parameter1: Type1, parameter2?: Type2) => ReturnType;
The type alias’s parameter names, like parameter1
, are mostly used for documentation purposes and have no bearing on the assignability of the function.
Function Type Annotation Syntax
Determining the anticipated “shape” of a function is made easier by TypeScript’s ability to express callable signatures using straightforward arrow type annotations.
type TwoNumberFunction = (a: number, b: number) => void;
// Applying the type alias to a constant function
const performOperation: TwoNumberFunction = (a, b) => {
// a and b are implicitly inferred as 'number' from the type alias
console.log(`Operation result: ${a + b}`);
};
performOperation(5, 10);
// performOperation(5, "10"); // Error: Type '"10"' is not assignable to type 'number'.
Output:
Operation result: 15
You do not need to annotate the arguments again in the function body when a function is assigned to a type alias (such as TwoNumberFunction
) because TypeScript uses contextual typing to deduce the parameter types (a
and b
) from the declaration.
Callable Interfaces and Overloads
The inability to provide overloads is the primary drawback of the arrow syntax, despite the fact that it is straightforward for specifying function types. You must utilise a full-bodied object literal syntax, usually within an interface or type alias definition, for complex function types, particularly ones that require several acceptable call signatures (overloading).
A type can be defined by an interface using one or more callable annotations.
interface Overloaded {
(foo: string): string;
(foo: number): number;
}
// Example usage of a type-safe callable interface:
declare const processor: Overloaded;
const strResult = processor("hello"); // Inferred type: string
const numResult = processor(100); // Inferred type: number
Optional Parameters
By default, all parameters in TypeScript are considered necessary. A TypeScript complaint will be raised if a function is called without the exact number of parameters that are expected.
In the function signature, you use the question mark (?
) next to the parameter name to indicate that a parameter might be left out when the function is called.
Syntax and Behaviour
- Placement: In the function specification, any necessary, non-optional parameters must come before any optional parameters.
- Type Implication: A parameter’s type immediately becomes
| undefined
when it is marked as optional. Its value inside the function will beundefined
if the argument is not passed in during invocation.
function getGreeting(name: string, title?: string): string {
if (title) {
return `Hello, ${title} ${name}.`;
}
return `Hello, ${name}.`;
}
// Case 1: Optional parameter omitted
console.log(getGreeting("Alice"));
// Case 2: Optional parameter supplied
console.log(getGreeting("Bob", "Dr."));
Output:
Hello, Alice.
Hello, Dr. Bob.
If the parameter is not provided in Case 1, title
is undefined
inside the function body. The if (title)
check then narrows the type of title
from string | undefined
to just string
within that block, allowing its safe use.
Default Parameters
If a parameter is explicitly passed undefined
or the caller fails to submit an argument, default parameters offer a way to provide a fallback value.
Syntax and Behaviour
The equality operator (=) is used to assign the default value following the parameter type declaration.
function calculateDiscount(price: number, rate: number = 0.50): number {
// If rate is omitted, 0.50 is used.
var discount = price * rate;
return discount;
}
// Case 1: Rate parameter is omitted, default rate (0.50) is used.
console.log("Discount 1:", calculateDiscount(1000));
// Case 2: Rate parameter is explicitly supplied, overriding the default.
console.log("Discount 2:", calculateDiscount(1000, 0.30));
Output:
Discount 1: 500
Discount 2: 300
The parameter is optional in the function call signature due to default parameters. A parameter cannot be specified both default and optional (using?
) at the same time. In the default parameter syntax, the optionality is handled implicitly.
Rest Parameters
In JavaScript, the unsafe arguments object is used to manage an infinite amount of arguments
supplied to a function; rest parameters offer a more contemporary, type-safe solution.
Syntax and Usage
- Notation: In the function signature, three periods (
…
) come before the last parameter name to indicate rest parameters. - Structure: All of the remaining arguments are gathered by the rest parameter into a single instance of an array.
- Constraints: A function can only have one rest parameter, and it needs to be the last argument in the list. An array type, usually stated with
Type[]
, must always be its type.
function combineNames(firstName: string, ...allOthers: string[]): string {
// 'allOthers' is guaranteed to be a string array inside the function body.
return firstName + ' ' + allOthers.join(' ');
}
// Case 1: No rest arguments provided (allOthers is an empty array [])
console.log(combineNames("Sarah"));
// Case 2: Multiple rest arguments provided
console.log(combineNames("John", "Paul", "George", "Ringo"));
Output:
Sarah
John Paul George Ringo
To ensure that all further values given are of the specified type (in this case, string
), type safety is enforced by the requirement that remainder parameters be of array type.
Function Overloading
TypeScript’s compile-time function overloading functionality lets you declare a function more than once with distinct call signatures. This is crucial for:
- Documentation: Outlining in detail the several ways a function should be invoked.
- Type Safety: Making sure the compiler accurately records the types of input and output according to the arguments sent in.
Multiple overload signatures and one implementation signature are necessary for a function overload.
Overload Signatures vs. Implementation
Function overloading is governed by stringent guidelines:
- Overload Signatures: The compiler verifies caller usage using these declarations of public functions. They specify every acceptable technique to call the function.
- Implementation Signature: The only function definition with the real logic is this one. Its list of parameters needs to be broad enough to include any possible combination of parameters specified by the overload signatures.
- Runtime: TypeScript deletes these type annotations when JavaScript is compiled, so function overloading has no runtime overhead. There is simply the implementation signature in the final JavaScript.
Overloading is used in the following example to determine padding according to the amount of arguments supplied (1, 2, or 4 arguments are permitted).
// 1. Overload Signatures (Public API):
// Case 1: All sides equal padding (1 argument)
function padding(all: number): { top: number, right: number, bottom: number, left: number };
// Case 2: Top/Bottom and Left/Right padding (2 arguments)
function padding(topAndBottom: number, leftAndRight: number): { top: number, right: number, bottom: number, left: number };
// Case 3: Individual padding for all four sides (4 arguments)
function padding(top: number, right: number, bottom: number, left: number): { top: number, right: number, bottom: number, left: number };
// 2. Implementation Signature (Must handle all cases defined above):
function padding(a: number, b?: number, c?: number, d?: number) {
if (b === undefined && c === undefined && d === undefined) {
// Case 1: Single argument provided (a)
b = c = d = a;
} else if (c === undefined && d === undefined) {
// Case 2: Two arguments provided (a, b)
c = a;
d = b;
}
// Case 3: Four arguments provided, or constructed above
return {
top: a,
right: b!, // Using non-null assertion or checks based on complexity
bottom: c!,
left: d!
};
}
// Valid calls (matched by overload signatures):
console.log("Call 1 (All):", padding(10));
console.log("Call 2 (TB/LR):", padding(5, 15));
console.log("Call 3 (Full):", padding(1, 2, 3, 4));
// Invalid call (Compiler Error: Not part of the available overloads):
// padding(1, 1, 1);
Output:
Call 1 (All): { top: 10, right: 10, bottom: 10, left: 10 }
Call 2 (TB/LR): { top: 5, right: 15, bottom: 5, left: 15 }
Call 3 (Full): { top: 1, right: 2, bottom: 3, left: 4 }
TypeScript will produce an error if a developer attempts to call padding(1, 1, 1)
since this signature is not included in the list of defined overloads. External callers cannot see the implementation signature; only the function logic can.